diff --git a/.github/workflows/main.yml b/.github/workflows/build-and-test.yml similarity index 96% rename from .github/workflows/main.yml rename to .github/workflows/build-and-test.yml index e33a07a197..395f88351a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/build-and-test.yml @@ -1,4 +1,4 @@ -name: "Main" +name: "build and test" on: pull_request: paths: diff --git a/.gitignore b/.gitignore index eb4dbedfae..fe565c7ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -293,3 +293,11 @@ samples/MicroserviceDemo/applications/ConsoleClientDemo/Logs/logs.txt modules/docs/app/Volo.DocsTestApp/Logs/logs.txt framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Logs/logs.txt samples/MicroserviceDemo/microservices/TenantManagementService.Host/Logs/logs.txt +/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package-lock.json +/npm/packs/bootstrap/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package-lock.json +/templates/app/angular/package-lock.json +/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index 14b044f8b1..72a47ddc4b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # ABP -[![Build Status](http://vjenkins.dynu.net:5480/job/abp/badge/icon)](http://ci.volosoft.com:5480/blue/organizations/jenkins/abp/activity) +![build and test](https://github.com/abpframework/abp/workflows/build%20and%20test/badge.svg) [![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) -[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) [![MyGet (with prereleases)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) +[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) This project is the next generation of the [ASP.NET Boilerplate](https://aspnetboilerplate.com/) web application framework. See [the announcement](https://blog.abp.io/abp/Abp-vNext-Announcement). diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index d0d829bbf3..c6010b777b 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -3,6 +3,12 @@ "texts": { "Permission:Organizations": "Organizations", "Permission:Manage": "Manage Organizations", + "Permission:DiscountRequests": "Discount Requests", + "Permission:DiscountManage": "Manage Discount Requests", + "Permission:Disable": "Disable", + "Permission:Enable": "Enable", + "Permission:EnableSendEmail": "Enable Send Email", + "Permission:SendEmail": "Send Email", "Permission:NpmPackages": "NPM Packages", "Permission:NugetPackages": "Nuget Packages", "Permission:Maintenance": "Maintenance", @@ -19,6 +25,7 @@ "Menu:Organizations": "Organizations", "Menu:Accounting": "Accounting", "Menu:Packages": "Packages", + "Menu:DiscountRequests": "Discount Requests", "NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Do you confirm that?", "NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Do you confirm that?", "ModuleDeletionWarningMessage": "This Module will be deleted. Do you confirm that?", @@ -119,6 +126,32 @@ "TotalPrice": "Total Price", "Generate": "Generate", "MissingQuantityField": "The quantity field is required!", - "MissingPriceField": "The Price field is required!" + "MissingPriceField": "The Price field is required!", + "CodeUsageStatus": "Status", + "Country": "Country", + "DeveloperCount": "Developer Count", + "RequestCode": "Request Code", + "WebSite": "Web Site", + "GithubUsername": "Github Username", + "PhoneNumber": "Phone Number", + "ProjectDescription": "Project Description", + "Referrer": "Referrer", + "DiscountRequests": "Discount Request", + "Copylink": "Copy Link", + "Disable": "Disable", + "Enable": "Enable", + "EnableSendEmail": "Enable Send Email", + "SendEmail": "Send Email", + "SuccessfullyDisabled": "Successfully Disabled", + "SuccessfullyEnabled": "Successfully Enabled", + "EmailSent": "Email Sent", + "SuccessfullySent": "Successfully Sent", + "SuccessfullyDeleted": "Successfully Deleted", + "DiscountRequestDeletionWarningMessage": "Discount request will be deleted" , + "BusinessType": "Business Type", + "TotalQuestionCount": "Total question count", + "RemainingQuestionCount": "Remaining question count", + "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount !", + "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero !" } } \ 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/cs/Entity-Framework-Core-PostgreSQL.md b/docs/cs/Entity-Framework-Core-PostgreSQL.md index 3a39c7485d..e09880c37c 100644 --- a/docs/cs/Entity-Framework-Core-PostgreSQL.md +++ b/docs/cs/Entity-Framework-Core-PostgreSQL.md @@ -10,9 +10,9 @@ Projekt `.EntityFrameworkCore` v řešení závisí na NuGet balíku [Volo.Abp.E Najděte třídu ***YourProjectName*EntityFrameworkCoreModule** v projektu `.EntityFrameworkCore`, odstraňte `typeof(AbpEntityFrameworkCoreSqlServerModule)` z atributu `DependsOn`, přidejte `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (také nahraďte `using Volo.Abp.EntityFrameworkCore.SqlServer;` za `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). -## UsePostgreSql() +## UseNpgsql() -Najděte volání `UseSqlServer()` v *YourProjectName*EntityFrameworkCoreModule.cs uvnitř projektu `.EntityFrameworkCore` a nahraďte za `UsePostgreSql()`. +Najděte volání `UseSqlServer()` v *YourProjectName*EntityFrameworkCoreModule.cs uvnitř projektu `.EntityFrameworkCore` a nahraďte za `UseNpgsql()`. Najděte volání `UseSqlServer()` v *YourProjectName*MigrationsDbContextFactory.cs uvnitř projektu `.EntityFrameworkCore.DbMigrations` a nahraďte za `UseNpgsql()`. diff --git a/docs/cs/Getting-Started-AspNetCore-Application.md b/docs/cs/Getting-Started-AspNetCore-Application.md index 2e8ea1a343..877976d037 100644 --- a/docs/cs/Getting-Started-AspNetCore-Application.md +++ b/docs/cs/Getting-Started-AspNetCore-Application.md @@ -57,7 +57,7 @@ namespace BasicAspNetCoreApplication app.UseStaticFiles(); app.UseRouting(); - app.UseMvcWithDefaultRouteAndArea(); + app.UseConfiguredEndpoints(); } } } diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md index 460623440f..94a0222243 100644 --- a/docs/en/API/Dynamic-CSharp-API-Clients.md +++ b/docs/en/API/Dynamic-CSharp-API-Clients.md @@ -39,7 +39,7 @@ Now, it's ready to create the client proxies. Example: ````csharp [DependsOn( typeof(AbpHttpClientModule), //used to create client proxies - typeof(BookStoreApplicationModule) //contains the application service interfaces + typeof(BookStoreApplicationContractsModule) //contains the application service interfaces )] public class MyClientAppModule : AbpModule { @@ -47,7 +47,7 @@ public class MyClientAppModule : AbpModule { //Create dynamic client proxies context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly + typeof(BookStoreApplicationContractsModule).Assembly ); } } @@ -142,12 +142,12 @@ The examples above have configured the "Default" remote service endpoint. You ma ````csharp context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - remoteServiceName: "BookStore" + typeof(BookStoreApplicationContractsModule).Assembly, + remoteServiceConfigurationName: "BookStore" ); ```` -`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. +`remoteServiceConfigurationName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. ### As Default Services @@ -155,7 +155,7 @@ When you create a service proxy for `IBookAppService`, you can directly inject t ````csharp context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, + typeof(BookStoreApplicationContractsModule).Assembly, asDefaultServices: false ); ```` diff --git a/docs/en/CLI.md b/docs/en/CLI.md index 75e3e027b4..d9b6832dd5 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -40,7 +40,7 @@ abp new Acme.BookStore #### Options * `--template` or `-t`: Specifies the template name. Default template name is `app`, which generates a web application. Available templates: - * `app` (default): [Application template](Startup-Templates/Application.md). Additional options: + * **`app`** (default): [Application template](Startup-Templates/Application.md). Additional options: * `--ui` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: * `mvc`: ASP.NET Core MVC. There are some additional options for this template: * `--tiered`: Creates a tiered solution where Web and Http API layers are physically separated. If not specified, it creates a layered solution which is less complex and suitable for most scenarios. @@ -51,10 +51,10 @@ abp new Acme.BookStore * `--mobile` or `-m`: Specifies the mobile application framework. Default framework is `react-native`. Available frameworks: * `none`: no mobile application. * `react-native`: React Native. - * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `module`: [Module template](Startup-Templates/Module.md). Additional options: + * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: + * `ef`: Entity Framework Core. + * `mongodb`: MongoDB. + * **`module`**: [Module template](Startup-Templates/Module.md). Additional options: * `--no-ui`: Specifies to not include the UI. This makes possible to create service-only modules (a.k.a. microservices - without UI). * `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory. * `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version. diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md index 8d64b031ec..b9890b3aca 100644 --- a/docs/en/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md @@ -114,7 +114,8 @@ public class MyIdentityUserManager : IdentityUserManager IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, IServiceProvider services, ILogger logger, ICancellationTokenProvider cancellationTokenProvider) : diff --git a/docs/en/Dapper.md b/docs/en/Dapper.md index 25bba118ec..0cb1a645d7 100644 --- a/docs/en/Dapper.md +++ b/docs/en/Dapper.md @@ -11,6 +11,9 @@ ABP does not encapsulate many functions for Dapper. ABP Dapper library provides Install and configure EF Core according to [EF Core's integrated documentation](Entity-Framework-Core.md). `Volo.Abp.Dapper` is the main nuget package for the Dapper integration. + +You can find it on NuGet Gallery: https://www.nuget.org/packages/Volo.Abp.Dapper + Install it to your project (for a layered application, to your data/infrastructure layer): ```shell diff --git a/docs/en/Entity-Framework-Core-PostgreSQL.md b/docs/en/Entity-Framework-Core-PostgreSQL.md index 328ca4d416..4c6099b809 100644 --- a/docs/en/Entity-Framework-Core-PostgreSQL.md +++ b/docs/en/Entity-Framework-Core-PostgreSQL.md @@ -10,9 +10,9 @@ This document explains how to switch to the **PostgreSQL** database provider for Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). -## UsePostgreSql() +## UseNpgsql() -Find `UseSqlServer()` call in *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project and replace with `UsePostgreSql()`. +Find `UseSqlServer()` call in *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project and replace with `UseNpgsql()`. Find `UseSqlServer()` call in *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project and replace with `UseNpgsql()`. diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md index b78ce1b9cf..9698b1dfb0 100644 --- a/docs/en/Getting-Started-AspNetCore-Application.md +++ b/docs/en/Getting-Started-AspNetCore-Application.md @@ -57,7 +57,7 @@ namespace BasicAspNetCoreApplication app.UseStaticFiles(); app.UseRouting(); - app.UseMvcWithDefaultRouteAndArea(); + app.UseConfiguredEndpoints(); } } } diff --git a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md index 10b1e05a4a..434d5f8bac 100644 --- a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md +++ b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md @@ -158,6 +158,17 @@ You can find the source code of the completed example [here](https://github.com/ ```` +* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! + + + * This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add + + ````csharp + options.Scope.Add("email"); + ```` + + to your openid configuration. + * Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error! * If you set your **CallbackPath** in appsettings as: @@ -169,18 +180,19 @@ You can find the source code of the completed example [here](https://github.com/ } ```` - your **Redirect URI** of your application in azure portal must be with domain like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. + your **Redirect URI** of your application in azure portal must be with domain like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. + +* Help! I keep getting ***AADSTS700051: The response_type 'token' is not enabled for the application.*** error! -* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! + * This error occurs when you request **token** (access token) along with **id_token** without enabling Access tokens on Azure portal app registrations. Simply tick **Access tokens** checkbox located on top of ID tokens to be able to request token aswell. +* Help! I keep getting ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** error! - * This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add - - ````csharp - options.Scope.Add("email"); - ```` - - to your openid configuration. + * This error occurs when you request **code** along with **id_token**. You need to add **client secret** on azure portal app registrations, under **Certificates & secrets** menu. Afterwards, you need to add openid configuration option like: + + ````csharp + options.ClientSecret = "Value of your secret on azure portal"; + ```` * How can I **debug/watch** which claims I get before they get mapped? @@ -188,13 +200,12 @@ You can find the source code of the completed example [here](https://github.com/ ````csharp options.Events.OnTokenValidated = (async context => - { - var claimsFromOidcProvider = context.Principal.Claims.ToList(); - await Task.CompletedTask; - }); + { + var claimsFromOidcProvider = context.Principal.Claims.ToList(); + await Task.CompletedTask; + }); ```` - ## See Also * [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index bee229dc27..8c7cca4037 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -174,6 +174,44 @@ ObjectExtensionManager.Instance The following sections explain the fundamental property configuration options. +#### Default Value + +A default value is automatically set for the new property, which is the natural default value for the property type, like `null` for `string`, `false` for `bool` or `0` for `int`. + +There are two ways to override the default value: + +##### DefaultValue Option + +`DefaultValue` option can be set to any value: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "MyIntProperty", + options => + { + options.DefaultValue = 42; + }); +```` + +##### DefaultValueFactory Options + +`DefaultValueFactory` can be set to a function that returns the default value: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "MyDateTimeProperty", + options => + { + options.DefaultValueFactory = () => DateTime.Now; + }); +```` + +`options.DefaultValueFactory` has a higher priority than the `options.DefaultValue` . + +> Tip: Use `DefaultValueFactory` option only if the default value may change over the time (like `DateTime.Now` in this example). If it is a constant value, then use the `DefaultValue` option. + #### CheckPairDefinitionOnMapping Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better. @@ -208,6 +246,15 @@ ObjectExtensionManager.Instance With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided. +#### Default Validation Attributes + +There are some attributes **automatically added** when you create certain type of properties; + +* `RequiredAttribute` is added for non nullable primitive property types (e.g. `int`, `bool`, `DateTime`...) and `enum` types. +* `EnumDataTypeAttribute` is added for enum types, to prevent to set invalid enum values. + +Use `options.Attributes.Clear();` if you don't want these attributes. + ### Custom Validation If you need, you can add a custom action that is executed to validate the extra properties. Example: diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md index a42d74c1c3..5f46b0f42e 100644 --- a/docs/en/Samples/Index.md +++ b/docs/en/Samples/Index.md @@ -35,6 +35,9 @@ While there is no Razor Pages & MongoDB combination, you can check both document * **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules. * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [EF Core database migrations document](../Entity-Framework-Core-Migrations.md) +* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) + * [SignalR Integration document](../SignalR-Integration.md) * **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI. * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [Widget documentation](../UI/AspNetCore/Widgets.md) diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index 567d043ef9..25eaf68055 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/docs/en/Samples/Microservice-Demo.md @@ -552,7 +552,7 @@ app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") || ctx.Request.Path.ToString().StartsWith("/Abp/"), app2 => { - app2.UseMvcWithDefaultRouteAndArea(); + app2.UseConfiguredEndpoints(); }); app.UseOcelot().Wait(); diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md new file mode 100644 index 0000000000..f318967888 --- /dev/null +++ b/docs/en/SignalR-Integration.md @@ -0,0 +1,237 @@ +# SignalR Integration + +> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides a SignalR integration packages those simplify the integration and usage. + +## Installation + +### Server Side + +It is suggested to use the [ABP CLI](CLI.md) to install this package. + +#### Using the ABP CLI + +Open a command line window in the folder of your project (.csproj file) and type the following command: + +```bash +abp add-package Volo.Abp.AspNetCore.SignalR +``` + +> You typically want to add this package to the web or API layer of your application, depending on your architecture. + +#### Manual Installation + +If you want to manually install; + +1. Add the [Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR) NuGet package to your project: + + ``` + Install-Package Volo.Abp.AspNetCore.SignalR + ``` + + Or use the Visual Studio NuGet package management UI to install it. + +2. Add the `AbpAspNetCoreSignalRModule` to the dependency list of your module: + +```csharp +[DependsOn( + //...other dependencies + typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +``` + +> You don't need to use the `services.AddSignalR()` and the `app.UseEndpoints(...)`, it's done by the `AbpAspNetCoreSignalRModule`. + +### Client Side + +Client side installation depends on your UI framework / client type. + +#### ASP.NET Core MVC / Razor Pages UI + +Run the following command in the root folder of your web project: + +````bash +yarn add @abp/signalr +```` + +> This requires to [install yarn](https://yarnpkg.com/) if you haven't install before. + +This will add the `@abp/signalr` to the dependencies in the `package.json` of your project: + +````json +{ + ... + "dependencies": { + ... + "@abp/signalr": "~2.7.0" + } +} +```` + +Run the `gulp` in the root folder of your web project: + +````bash +gulp +```` + +This will copy the SignalR JavaScript files into your project: + +![signal-js-file](images/signal-js-file.png) + +Finally, add the following code to your page/view to include the `signalr.js` file + +````xml +@section scripts { + +} +```` + +It requires to add `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` to your page/view. + +> You could add the `signalr.js` file in a standard way. But using the `SignalRBrowserScriptContributor` has additional benefits. See the [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md) and [Bundling & Minification](UI/AspNetCore/Bundling-Minification.md) documents for details. + +That's all. you can use the [SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client) in your page. + +#### Other UI Frameworks / Clients + +Please refer to [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) for other type of clients. + +## The ABP Framework Integration + +This section covers the additional benefits when you use the ABP Framework integration packages. + +### Hub Route & Mapping + +ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name. + +Example: + +````csharp +public class MessagingHub : Hub +{ + //... +} +```` + +The hub route will be `/signalr-hubs/messasing` for the `MessasingHub`: + +* Adding a standard `/signalr-hubs/` prefix +* Continue with the **camel case** hub name, without the `Hub` suffix. + +If you want to specify the route, you can use the `HubRoute` attribute: + +````csharp +[HubRoute("/my-messasing-hub")] +public class MessagingHub : Hub +{ + //... +} +```` + +### AbpHub Base Classes + +Instead of the standard `Hub` and `Hub` classes, you can inherit from the `AbpHub` or `AbpHub` which hve useful base properties like `CurrentUser`. + +Example: + +````csharp +public class MessagingHub : AbpHub +{ + public async Task SendMessage(string targetUserName, string message) + { + var currentUserName = CurrentUser.UserName; //Access to the current user info + var txt = L["MyText"]; //Localization + } +} +```` + +> While you could inject the same properties into your hub constructor, this way simplifies your hub class. + +### Manual Registration / Mapping + +ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) as a **transient service**. If you want to **disable auto dependency injection** registration for your hub class, just add a `DisableConventionalRegistration` attribute. You can still register your hub class to dependency injection in the `ConfigureServices` method of your module if you like: + +````csharp +context.Services.AddTransient(); +```` + +When **you or ABP** register the class to the dependency injection, it is automatically mapped to the endpoint route configuration just as described in the previous sections. You can use `DisableAutoHubMap` attribute if you want to manually map your hub class. + +For manual mapping, you have two options: + +1. Use the `AbpSignalROptions` to add your map configuration (in the `ConfigureServices` method of your [module](Module-Development-Basics.md)), so ABP still performs the endpoint mapping for your hub: + +````csharp +Configure(options => +{ + options.Hubs.Add( + new HubConfig( + typeof(MessagingHub), //Hub type + "/my-messaging/route", //Hub route (URL) + hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + } + ) + ); +}); +```` + +This is a good way to provide additional SignalR options. + +If you don't want to disable auto hub map, but still want to perform additional SignalR configuration, use the `options.Hubs.AddOrUpdate(...)` method: + +````csharp +Configure(options => +{ + options.Hubs.AddOrUpdate( + typeof(MessagingHub), //Hub type + config => //Additional configuration + { + config.RoutePattern = "/my-messaging-hub"; //override the default route + config.ConfigureActions.Add(hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); + } + ); +}); +```` + +This is the way you can modify the options of a hub class defined in a depended module (where you don't have the source code access). + +2. Change `app.UseConfiguredEndpoints` in the `OnApplicationInitialization` method of your [module](Module-Development-Basics.md) as shown below (added a lambda method as the parameter). + +````csharp +app.UseConfiguredEndpoints(endpoints => +{ + endpoints.MapHub("/my-messaging-hub", options => + { + options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); +}); +```` + +### UserIdProvider + +ABP implements SignalR's `IUserIdProvider` interface to provide the current user id from the `ICurrentUser` service of the ABP framework (see [the current user service](CurrentUser.md)), so it will be integrated to the authentication system of your application. The implementing class is the `AbpSignalRUserIdProvider`, if you want to change/override it. + +## Example Application + +See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messages between (authenticated) users. + +![signalr-demo-chat](images/signalr-demo-chat.png) + +## Remarks + +ABP Framework doesn't change the SignalR. It works in your ABP Framework based application just like any other ASP.NET Core application. + +Refer to the Microsoft's documentation to [host and scale](https://docs.microsoft.com/en-us/aspnet/core/signalr/scale) your application, integrate to [Azure](https://docs.microsoft.com/en-us/aspnet/core/signalr/publish-to-azure-web-app) or [Redis backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)... etc. + +## See Also + +* [Microsoft SignalR documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) \ No newline at end of file diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 18a360c5c8..c71787f598 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -204,13 +204,14 @@ Inline localization uses the [localization system](Localization.md) to localize Assuming you need to send an email to a user to reset her/his password. Here, the template content: ```` -{%{{{L "ResetMyPassword"}}}%} +{%{{{L "ResetMyPassword" model.name}}}%} ```` `L` function is used to localize the given key based on the current user culture. You need to define the `ResetMyPassword` key inside your localization file: ````json -"ResetMyPassword": "Click here to reset your password" +"ResetMyPasswordTitle": "Reset my password", +"ResetMyPassword": "Hi {0}, Click here to reset your password" ```` You also need to declare the localization resource to be used with this template, inside your template definition provider class: @@ -234,6 +235,7 @@ var result = await _templateRenderer.RenderAsync( "PasswordReset", //the template name new PasswordResetModel { + Name = "john", Link = "https://abp.io/example-link?userId=123&token=ABC" } ); @@ -242,7 +244,7 @@ var result = await _templateRenderer.RenderAsync( You will see the localized result: ````csharp -Click here to reset your password +Hi john, Click here to reset your password ```` > If you define the [default localization resource](Localization.md) for your application, then no need to declare the resource type for the template definition. diff --git a/docs/en/UI/Angular/Confirmation-Service.md b/docs/en/UI/Angular/Confirmation-Service.md index 2d1c5c1843..16dfa9fd94 100644 --- a/docs/en/UI/Angular/Confirmation-Service.md +++ b/docs/en/UI/Angular/Confirmation-Service.md @@ -53,7 +53,7 @@ this.confirmation - `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. + - `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape or clicking the backdrop. If you are not interested in the confirmation status, you do not have to subscribe to the returned observable: @@ -70,6 +70,7 @@ Options can be passed as the third parameter to `success`, `warn`, `error`, and const options: Partial = { hideCancelBtn: false, hideYesBtn: false, + dismissible: false, cancelText: 'Close', yesText: 'Confirm', messageLocalizationParams: ['Demo'], @@ -83,10 +84,11 @@ this.confirmation.warn( ); ``` -- `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` +- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false`. +- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false`. +- `dismissible` option allows dismissing the confirmation popup by pressing escape or clicking the backdrop. Default value is `true`. +- `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. diff --git a/docs/en/UI/Angular/List-Service.md b/docs/en/UI/Angular/List-Service.md new file mode 100644 index 0000000000..b3c8571866 --- /dev/null +++ b/docs/en/UI/Angular/List-Service.md @@ -0,0 +1,165 @@ +# Querying Lists Easily with ListService + +`ListService` is a utility service to provide an easy pagination, sorting, and search implementation. + + + +## Getting Started + +`ListService` is **not provided in root**. The reason is, this way, it will clear any subscriptions on component destroy. You may use the optional `LIST_QUERY_DEBOUNCE_TIME` token to adjust the debounce behavior. + +```js +import { ListService } from '@abp/ng.core'; +import { BookDto } from '../models'; +import { BookService } from '../services'; + +@Component({ + /* class metadata here */ + providers: [ + // [Required] + ListService, + + // [Optional] + // Provide this token if you want a different debounce time. + // Default is 300. Cannot be 0. Any value below 100 is not recommended. + { provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 500 }, + ], + template: ` + + `, +}) +class BookComponent { + items: BookDto[] = []; + count = 0; + + constructor( + public readonly list: ListService, + private bookService: BookService, + ) {} + + ngOnInit() { + // A function that gets query and returns an observable + const bookStreamCreator = query => this.bookService.getList(query); + + this.list.hookToQuery(bookStreamCreator).subscribe( + response => { + this.items = response.items; + this.count = response.count; + // If you use OnPush change detection strategy, + // call detectChanges method of ChangeDetectorRef here. + } + ); // Subscription is auto-cleared on destroy. + } +} +``` + +> Noticed `list` is `public` and `readonly`? That is because we will use `ListService` properties directly in the component's template. That may be considered as an anti-pattern, but it is much quicker to implement. You can always use public component properties instead. + +Place `ListService` properties into the template like this: + +```html + + + + + + + {%{{{ '::Name' | abpLocalization }}}%} + + + + + + + + {%{{{ data.name }}}%} + + +``` + +## Usage with Observables + +You may use observables in combination with [AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe) of Angular instead. Here are some possibilities: + +```ts + book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query)); +``` + +```html + + + + + + +``` + +...or... + + +```ts + @Select(BookState.getBooks) + books$: Observable; + + @Select(BookState.getBookCount) + bookCount$: Observable; + + ngOnInit() { + this.list.hookToQuery((query) => this.store.dispatch(new GetBooks(query))).subscribe(); + } +``` + +```html + + + + +``` + +## How to Refresh Table on Create/Update/Delete + +`ListService` exposes a `get` method to trigger a request with the current query. So, basically, whenever a create, update, or delete action resolves, you can call `this.list.get();` and it will call hooked stream creator again. + +```ts +this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get); +``` + +...or... + +```ts +this.bookService.createByInput(form.value) + .subscribe(() => { + this.list.get(); + + // Other subscription logic here + }) +``` + +## How to Implement Server-Side Search in a Table + +`ListService` exposes a `filter` property that will trigger a request with the current query and the given search string. All you need to do is to bind it to an input element with two-way binding. + +```html + + + +``` diff --git a/docs/en/UI/Angular/Track-By-Service.md b/docs/en/UI/Angular/Track-By-Service.md index e6b560a2eb..447cc4505a 100644 --- a/docs/en/UI/Angular/Track-By-Service.md +++ b/docs/en/UI/Angular/Track-By-Service.md @@ -111,3 +111,9 @@ class DemoComponent { trackByTenantAccountId = trackByDeep('tenant', 'account', 'id'); } ``` + + + +## What's Next? + +- [ListService](./List-Service.md) diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md b/docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md index df23fd649c..8f664a80a5 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md @@ -88,6 +88,8 @@ You can set some of the attributes on your c# property, or directly on html tag. * `label`: Sets the label for input. * `display-required-symbol`: Adds the required symbol (*) to label if input is required. Default `True`. +`asp-format`, `name` and `value` attributes of [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-input-tag-helper) are also valid for `abp-input` tag helper. + ### Label & Localization You can set label of your input in different ways: @@ -258,4 +260,4 @@ You can set some of the attributes on your c# property, or directly on html tag. #### Tag Attributes - `asp-items`: Sets the select data. This Should be a list of SelectListItem. -- `Inline`: If true, radio buttons will be in single line, next to each other. If false, they will be under each other. \ No newline at end of file +- `Inline`: If true, radio buttons will be in single line, next to each other. If false, they will be under each other. diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md index 0cc63d49e8..9969bcf3cc 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } } diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 5f6203054e..47789023c4 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -383,6 +383,10 @@ { "text": "TrackByService", "path": "UI/Angular/Track-By-Service.md" + }, + { + "text": "ListService", + "path": "UI/Angular/List-Service.md" } ] }, diff --git a/docs/en/images/signal-js-file.png b/docs/en/images/signal-js-file.png new file mode 100644 index 0000000000..19fc6cefe3 Binary files /dev/null and b/docs/en/images/signal-js-file.png differ diff --git a/docs/en/images/signalr-demo-chat.png b/docs/en/images/signalr-demo-chat.png new file mode 100644 index 0000000000..120d7eda6e Binary files /dev/null and b/docs/en/images/signalr-demo-chat.png differ diff --git a/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md b/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md index eae86d6117..82473f9bbd 100644 --- a/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md +++ b/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md @@ -39,7 +39,7 @@ public class MyClientAppModule : AbpModule ````csharp [DependsOn( typeof(AbpHttpClientModule), //用来创建客户端代理 - typeof(BookStoreApplicationModule) //包含应用服务接口 + typeof(BookStoreApplicationContractsModule) //包含应用服务接口 )] public class MyClientAppModule : AbpModule { @@ -47,7 +47,7 @@ public class MyClientAppModule : AbpModule { //创建动态客户端代理 context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly + typeof(BookStoreApplicationContractsModule).Assembly ); } } @@ -142,12 +142,12 @@ public override void ConfigureServices(ServiceConfigurationContext context) ````csharp context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - remoteServiceName: "BookStore" + typeof(BookStoreApplicationContractsModule).Assembly, + remoteServiceConfigurationName: "BookStore" ); ```` -`remoteServiceName`参数会匹配通过`AbpRemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点. +`remoteServiceConfigurationName`参数会匹配通过`AbpRemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点. ### 作为默认服务 @@ -155,7 +155,7 @@ context.Services.AddHttpClientProxies( ````csharp context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, + typeof(BookStoreApplicationContractsModule).Assembly, asDefaultServices: false ); ```` diff --git a/docs/zh-Hans/Customizing-Application-Modules-Guide.md b/docs/zh-Hans/Customizing-Application-Modules-Guide.md index 7e799c885d..bf3404ddb7 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Guide.md +++ b/docs/zh-Hans/Customizing-Application-Modules-Guide.md @@ -17,7 +17,7 @@ ABP框架提供的设计旨在支持构建完全[模块化的应用程序](Modul 这种方法具有以下优点: * 你的解决方案会非常**干净**,只包含你**自己的应用程序代码**. -* 你可以**很简单的**升级模块到最新的可用模板. `abp update` [CLI](CLI.md) 命令会使更新变的更加简单. 通过这种方式, 你可以获得**最新功能和Bus修复**. +* 你可以**很简单的**升级模块到最新的可用模板. `abp update` [CLI](CLI.md) 命令会使更新变的更加简单. 通过这种方式, 你可以获得**最新功能和Bug修复**. 然而有一个缺点: diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md index caa448a14a..94a2120059 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md @@ -107,22 +107,25 @@ public class MyIdentityUserAppService : IdentityUserAppService public class MyIdentityUserManager : IdentityUserManager { public MyIdentityUserManager( - IdentityUserStore store, + IdentityUserStore store, + IIdentityRoleRepository roleRepository, + IIdentityUserRepository userRepository, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, ILogger logger, - ICancellationTokenProvider cancellationTokenProvider - ) : base( - store, + ICancellationTokenProvider cancellationTokenProvider) : + base(store, + roleRepository, + userRepository, optionsAccessor, passwordHasher, userValidators, - passwordValidators, + passwordValidators, keyNormalizer, errors, services, diff --git a/docs/zh-Hans/Dapper.md b/docs/zh-Hans/Dapper.md index 60289abd00..1f6f1aee61 100644 --- a/docs/zh-Hans/Dapper.md +++ b/docs/zh-Hans/Dapper.md @@ -1,16 +1,16 @@ # Dapper 集成 -由于Dapper的思想是sql语句优先, 且主要为`IDbConnection`接口提供了一些扩展方法. +Dapper 是轻量化的数据库提供者,使用Dapper主要的好处是编写T-SQL查询,它为 `IDbConnection` 接口提供了一些扩展方法. -Abp并没有为Dapper封装太多功能. Abp Dapper在Abp EntityFrameworkCore的基础上提供了`DapperRepository`基类, 在其中提供了Dapper需要的`IDbConnection`和`IDbTransaction`属性. - -这两个属性可以和[工作单元](Unit-Of-Work.md)很好的配合. +ABP没有为Dapper封装许多功能, ABP Dapper库在ABP EntityFrameworkCore的基础上提供了 `DapperRepository` 基类,在其中提供了Dapper需要的`IDbConnection`和`IDbTransaction`属性,这两个属性可以和[工作单元](Unit-Of-Work.md)很好的配合. ## 安装 请先根据[EF Core的集成文档](Entity-Framework-Core.md)安装并配置好EF Core. -`Volo.Abp.Dapper`是Dapper集成的主要nuget包. 将其安装到你的项目中(在分层应用程序中适用于 数据访问/基础设施层): +`Volo.Abp.Dapper`是Dapper集成的主要[nuget包](https://www.nuget.org/packages/Volo.Abp.Dapper). + +将其安装到你的项目中(在分层应用程序中适用于 数据访问/基础设施层): ```shell Install-Package Volo.Abp.Dapper diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md index caf5778753..77f5ac5cd7 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Migrations.md +++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md @@ -550,6 +550,8 @@ public class IdentityRoleExtendingService : ITransientDependency 你需要做的就是如上所诉使用 `ObjectExtensionManager` 定义额外属性, 然后你就可以使得 `GetProperty` 和 `SetProperty` 方法对实体的属性进行get/set,但是这时它存储在数据库表的单独字段中. +参阅[实体扩展系统](Customizing-Application-Modules-Extending-Entities.md)了解更多. + ###### 创建新表 你可以创建**自己的表**来存储属性,而不是创建新实体并映射到同一表. 你通常复制原始实体的一些值. 例如可以将 `Name` 字段添加到你自己的表中,它是原表中 `Name` 字段的副本. diff --git a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md index 08f8f405d1..350cff65b0 100644 --- a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md +++ b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md @@ -10,9 +10,9 @@ 在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). -## UsePostgreSql() +## UseNpgsql() -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UsePostgreSql()`. 检查下列文件: +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseNpgsql()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. * `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md index 9d195562fd..0334609bd3 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md @@ -30,10 +30,8 @@ ABP是一个模块化框架,它需要一个**启动 (根) 模块**继承自``Abp ````C# using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Volo.Abp; -using Volo.Abp.AspNetCore.Modularity; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Modularity; @@ -42,7 +40,8 @@ namespace BasicAspNetCoreApplication [DependsOn(typeof(AbpAspNetCoreMvcModule))] public class AppModule : AbpModule { - public override void OnApplicationInitialization(ApplicationInitializationContext context) + public override void OnApplicationInitialization( + ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); @@ -51,8 +50,14 @@ namespace BasicAspNetCoreApplication { app.UseDeveloperExceptionPage(); } + else + { + app.UseExceptionHandler("/Error"); + } - app.UseMvcWithDefaultRoute(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseConfiguredEndpoints(); } } } diff --git a/docs/zh-Hans/Getting-Started.md b/docs/zh-Hans/Getting-Started.md index b60fccfaa2..1b156b070d 100644 --- a/docs/zh-Hans/Getting-Started.md +++ b/docs/zh-Hans/Getting-Started.md @@ -111,6 +111,7 @@ abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mon {{end}} {{ else if UI == "NG" }} + 在创建的解决方案中有三个文件夹: ![](images/solution-files-non-mvc.png) @@ -120,6 +121,7 @@ abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mon * `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 }} diff --git a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md index 8b282bbe56..34aab9fa46 100644 --- a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md +++ b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md @@ -157,6 +157,16 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); ```` +* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! + + * 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 + + ````csharp + options.Scope.Add("email"); + ```` + + 到你的 openid 配置. + * Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误! * 如果你在appsettings设置 **CallbackPath** 为: @@ -170,15 +180,17 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi 你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc` 的, 而不仅是 `/signin-azuread-oidc`. -* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! +* Help! 我一直得到 ***AADSTS700051: The response_type 'token' is not enabled for the application.*** 错误! - * 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 + * 当你请求**token**(访问令牌)和**id_token**时没有在Azure门户应用程序启用访问令牌时会发生这个错误,只需勾选ID令牌顶部的**访问令牌**复选框即可同时请求令牌. - ````csharp - options.Scope.Add("email"); - ```` +* Help! 我一直得到 ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** 错误! - 到你的 openid 配置. + * 当你与 **id_token** 一起请求 **code**时,你需要在Azure门户的**证书和机密**菜单下添加**证书和机密**. 然后你需要添加openid配置选项: + + ````csharp + options.ClientSecret = "Value of your secret on azure portal"; + ```` * 如何**调试/监视**在映射之前获得的声明? diff --git a/docs/zh-Hans/Object-Extensions.md b/docs/zh-Hans/Object-Extensions.md index f925a8730a..22f4ae9322 100644 --- a/docs/zh-Hans/Object-Extensions.md +++ b/docs/zh-Hans/Object-Extensions.md @@ -176,6 +176,44 @@ ObjectExtensionManager.Instance `options` 有一个名为 `Configuration` 的字典,该字典存储对象扩展定义甚至可以扩展. EF Core使用它来将其他属性映射到数据库中的表字段. 请参阅[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md). +#### 默认值 + +自动为新属性设置默认值,默认值是属性类型的自然默认值,例如: `string`: `null` , `bool`: `false` 或 `int`: `0`. + +有两种方法可以覆盖默认值: + +##### DefaultValue 选项 + +`DefaultValue` 选项可以设置任何值: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "MyIntProperty", + options => + { + options.DefaultValue = 42; + }); +```` + +##### DefaultValueFactory 选项 + +`DefaultValueFactory` 可以设置返回默认值的函数: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "MyDateTimeProperty", + options => + { + options.DefaultValueFactory = () => DateTime.Now; + }); +```` + +`options.DefaultValueFactory` 比 `options.DefaultValue` 优先级要高. + +> 提示: 只有在默认值可能发生变化时(如示例中的`DateTime.Now;`) 才使用 `DefaultValueFactory`,如果是一个常量请使用 `DefaultValue` 选项. + #### CheckPairDefinitionOnMapping 控制在映射两个可扩展对象时如何检查属性定义. 请参阅*对象到对象映射*部分,了解 `CheckPairDefinitionOnMapping` 选项. diff --git a/docs/zh-Hans/Samples/Index.md b/docs/zh-Hans/Samples/Index.md new file mode 100644 index 0000000000..12fcf0ac97 --- /dev/null +++ b/docs/zh-Hans/Samples/Index.md @@ -0,0 +1,62 @@ +# 示例应用 + +这些是ABP框架创建的官方示例. 这些示例大部分在[abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub 仓库. + +### 微服务示例 + +演示如何基于微服务体系结构构建系统的完整解决方案. + +* [示例的文档](Microservice-Demo.md) +* [源码](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo) +* [微服务架构文档](../Microservice-Architecture.md) + +### Book Store + +一个简单的CRUD应用程序,展示了使用ABP框架开发应用程序的基本原理. 使用不同的技术实现了相同的示例: + +* **Book Store: Razor Pages UI & Entity Framework Core** + + * [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore) + +* **Book Store: Angular UI & MongoDB** + + * [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +* **Book Store: Modular application (Razor Pages UI & EF Core)** + + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) + +如果没有Razor Pages & MongoDB 结合,但你可以检查两个文档来理解它,因为DB和UI不会互相影响. + +### 其他示例 + +* **Entity Framework 迁移**: 演示如何将应用程序拆分为多个数据库的解决方案. 每个数据库包含不同的模块. + * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) + * [EF Core数据库迁移文档](../Entity-Framework-Core-Migrations.md) +* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. + * [源码](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) + * [SignalR 集成文档](../SignalR-Integration.md) +* **Dashboard Demo**: 一个简单的应用程序,展示了如何在ASP.NET Core MVC UI中使用widget系统. + * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) + * [Widget 文档](../UI/AspNetCore/Widgets.md) +* **RabbitMQ 事件总线 Demo**: 由两个通过RabbitMQ集成的分布式事件相互通信的应用程序组成的解决方案. + * [源码](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) + * [分布式事件总线文档](../Distributed-Event-Bus.md) + * [RabbitMQ 分布式事件总线集成文档](../Distributed-Event-Bus-RabbitMQ-Integration.md) +* **文本模板 Demo**: 文本模板系统的不同用例. + * [源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) + * [文本模板文档](../Text-Templating.md) +* **自定义认证**: 如何为ASP.NET Core MVC / Razor Pages应用程序自定义身份验证的解决方案. + * [源码](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization) + * 相关 "[How To](../How-To/Index.md)" 文档: + * [Azure Active Directory 认证](../How-To/Azure-Active-Directory-Authentication-MVC.md) + * [自定义登录页面](../How-To/Customize-Login-Page-MVC.md) + * [自定义 SignIn Manager](../How-To/Customize-SignIn-Manager.md) +* **空的ASP.NET Core应用程序**: 从基本的ASP.NET Core应用程序使用ABP框架. + * [源码](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) + * [文档](../Getting-Started-AspNetCore-Application.md) +* **空的控制台应用程序**: 从基本的控制台应用程序安装ABP框架. + * [源码](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication) + * [文档](../Getting-Started-Console-Application.md) \ No newline at end of file diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md index 40cb42cda6..b0134eddde 100644 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ b/docs/zh-Hans/Samples/Microservice-Demo.md @@ -553,7 +553,7 @@ app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") || ctx.Request.Path.ToString().StartsWith("/Abp/"), app2 => { - app2.UseMvcWithDefaultRouteAndArea(); + app2.UseConfiguredEndpoints(); }); app.UseOcelot().Wait(); diff --git a/docs/zh-Hans/SignalR-Integration.md b/docs/zh-Hans/SignalR-Integration.md new file mode 100644 index 0000000000..8045bbd394 --- /dev/null +++ b/docs/zh-Hans/SignalR-Integration.md @@ -0,0 +1,227 @@ +# SignalR 集成 + +> 你可以按照[标准的微软教程](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signal)添加[SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction)到你的应用程序,但ABP提供了简化集成的SignalR集成包. + +## 安装 + +### 服务器端 + +建议使用[ABP CLI](CLI.md)安装包. + +#### 使用 ABP CLI + +在项目的文件夹(.csproj文件)中打开命令行窗口,然后输入以下命令: + +```bash +abp add-package Volo.Abp.AspNetCore.SignalR +``` + +> 你通常需要将此软件包添加到应用程序的Web或API层,具体取决于你的架构. + +#### 手动安装 + +如果你想手动安装: + +1. 添加[Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR)NuGet包到你的项目: + + ``` + Install-Package Volo.Abp.AspNetCore.SignalR + ``` + + 或者使用VisualStudio提供的UI安装 + +2. 添加 `AbpAspNetCoreSignalRModule` 到你的模块的依赖列表. + +```csharp +[DependsOn( + //...other dependencies + typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +``` + +> 你不需要 `services.AddSignalR()` 和 `app.UseEndpoints(...)`,它们在 `AbpAspNetCoreSignalRModule` 中已经添加了. + +### 客户端 + +客户端安装取决于你的UI框架/客户端类型. + +#### ASP.NET Core MVC / Razor Pages UI + +在你的Web项目的根文件夹中运行以下命令: + +````bash +yarn add @abp/signalr +```` + +> 需要 [yarn](https://yarnpkg.com/) 环境. + +它会添加 `@abp/signalr` 到你的项目中的 `package.json` 依赖项: + +````json +{ + ... + "dependencies": { + ... + "@abp/signalr": "~2.7.0" + } +} +```` + +在你的Web项目的根文件夹中运行 `gulp`: + +````bash +gulp +```` + +它会将SignalR JavaScript文件拷贝到你的项目: + +![signal-js-file](images/signal-js-file.png) + +最后将以下代码添加到页面/视图中, 添加包含 `signalr.js` 文件: + +````xml +@section scripts { + +} +```` + +它需要将 `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` 添加到你的页面/视图. + +> 你可以用标准方式添加 `signalr.js` 文件. 但是使用 `SignalRBrowserScriptContributor` 具有其他好处. 有关详细信息,请参见[客户端程序包管理](UI/AspNetCore/Client-Side-Package-Management.md)和[捆绑和压缩文档](UI/AspNetCore/Bundling-Minification.md). + +这就是全部了,你可以在你的页面使用[SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client). + +#### 其他的UI框架/客户端 + +其他类型的客户端请参考[微软文档](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction). + +## ABP框架集成 + +本节介绍了使用ABP框架集成包的其他好处. + +### Hub 路由与Mapping + +ABP自动将所有集线器注册到[依赖注入](Dependency-Injection.md)(做为transient)并映射集线器端点. 因此你不需要使用 `app.UseEndpoints(...)` 即可映射你的集线器.集线器路由(URL)通常是根据你的集线器名称确定. + +示例: + +````csharp +public class MessagingHub : Hub +{ + //... +} +```` + +`MessasingHub` 集线器的路由为 `/signalr-hubs/messasing`: + +* 添加了标准 `/signalr-hubs/` 前缀. +* 使用**驼峰命名**集线器名称,不包含 `Hub` 后缀. + +如果你想指定路由,你可以使用 `HubRoute` attribute: + +````csharp +[HubRoute("/my-messasing-hub")] +public class MessagingHub : Hub +{ + //... +} +```` + +### AbpHub 基类 + +你可以从 `AbpHub` 或 `AbpHub` 继承标准的 `Hub` 和 `Hub` 类,它们具有实用的基本属性,如 `CurrentUser`. + +示例: + +````csharp +public class MessagingHub : AbpHub +{ + public async Task SendMessage(string targetUserName, string message) + { + var currentUserName = CurrentUser.UserName; //Access to the current user info + var txt = L["MyText"]; //Localization + } +} +```` + +> 虽然可以将相同的属性注入到集线器构造函数中,但是这种方式简化了集线器类. + +### 手动注册/Mapping + +ABP会自动将所有集线器注册到[依赖注入](Dependency-Injection.md)作为**transient service**. 如果想要禁用集线器类**自动添加依赖注入**,只需要使用 `DisableConventionalRegistration` attribute. 如果愿意,你仍然可以在模块的 `ConfigureServices` 方法中注册集线器类: + +````csharp +context.Services.AddTransient(); +```` + +当**你或ABP**将类注册到依赖注入时,如前几节所述,它会自动映射到端点路由配置. 如果要手动映射集线器类,你可以使用 `DisableAutoHubMap` attribute. + +对于手动映射,你有两个选择: + +1. 使用 `AbpSignalROptions` 添加map配置(在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中),ABP会为集线器执行端点映射: + +````csharp +Configure(options => +{ + options.Hubs.Add( + new HubConfig( + typeof(MessagingHub), //Hub type + "/my-messaging/route", //Hub route (URL) + hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + } + ) + ); +}); +```` + +这是提供其他SignalR选项的好方式. + +如果你不想禁用自动集线器map,但仍想执行其他SignalR配置,可以使用 `options.Hubs.AddOrUpdate(...)` 方法: + +````csharp +Configure(options => +{ + options.Hubs.AddOrUpdate( + typeof(MessagingHub), //Hub type + config => //Additional configuration + { + config.RoutePattern = "/my-messaging-hub"; //override the default route + config.ConfigureActions.Add(hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); + } + ); +}); +```` + +你可以通过这种方式修改在依赖模块(没有源代码访问权限)中定义的集线器类的选项. + +2. 在[模块](Module-Development-Basics.md)的 `OnApplicationInitialization` 方法中更改 `app.UseConfiguredEndpoints`(添加了lambda方法作为参数). + +````csharp +app.UseConfiguredEndpoints(endpoints => +{ + endpoints.MapHub("/my-messaging-hub", options => + { + options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); +}); +```` + +### UserIdProvider + +ABP实现 `SignalR` 的 `IUserIdProvider` 接口,从ABP框架的 `ICurrentUser` 服务提供当前用户ID(请参阅[当前用户服务](CurrentUser.md)),它将集成到应用程序的身份验证系统中,实现类是 `AbpSignalRUserIdProvider` (如果你想更改/覆盖它). + +## 示例应用程序 + +参阅 [SignalR集成Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo),它有一个简单的聊天页面,可以在(经过身份验证的)用户之间发送消息. + +![signalr-demo-chat](images/signalr-demo-chat.png) \ No newline at end of file diff --git a/docs/zh-Hans/Text-Templating.md b/docs/zh-Hans/Text-Templating.md new file mode 100644 index 0000000000..3d3941c607 --- /dev/null +++ b/docs/zh-Hans/Text-Templating.md @@ -0,0 +1,455 @@ +# 文本模板 + +## 介绍 + +ABP框架提供了一个简单有效的文本模板系统,文本模板用于动态渲染基于模板和模型(数据对象)内容: + +***TEMPLATE + MODEL ==render==> RENDERED CONTENT*** + +它非常类似于 ASP.NET Core Razor View (或 Page): + +*RAZOR VIEW (or PAGE) + MODEL ==render==> HTML CONTENT* + +你可以将渲染的输出用于任何目的,例如发送电子邮件或准备一些报告. + +### 示例 + +Here, a simple template: + +```` +Hello {%{{{model.name}}}%} :) +```` + +你可以定义一个含有 `Name` 属性的类来渲染这个模板: + +````csharp +public class HelloModel +{ + public string Name { get; set; } +} +```` + +如果你使用 `Name` 为 `John` 的 `HelloModel` 渲染模板,输出为: + +```` +Hello John :) +```` + +模板渲染引擎非常强大; + +* 它基于 [Scriban](https://github.com/lunet-io/scriban) 库, 所以它支持 **条件逻辑**, **循环** 等. +* 模板内容 **可以本地化**. +* 你可以定义 **布局模板** 在渲染其他模板中用做布局. +* 对于高级场景,你可以传递任何对象到模板上下文. + +### 源码 + +这里是本文开发和引用的[示例应用程序源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). + +## 安装 + +推荐使用 [ABP CLI](CLI.md) 安装包. + +### 使用 ABP CLI + +在项目目录(.csproj file)打开命令行窗口运行以下命令: + +````bash +abp add-package Volo.Abp.TextTemplating +```` + +### 手动安装 + +如果你想要手动安装; + +1. 添加 [Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) NuGet包到你的项目: + +```` +Install-Package Volo.Abp.TextTemplating +```` + +2. 添加 `AbpTextTemplatingModule` 到你的模块依赖列表: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpTextTemplatingModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +```` + +## 定义模板 + +在渲染模板之前,需要定义它. 创建一个继承自 `TemplateDefinitionProvider` 的类: + +````csharp +public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider +{ + public override void Define(ITemplateDefinitionContext context) + { + context.Add( + new TemplateDefinition("Hello") //template name: "Hello" + .WithVirtualFilePath( + "/Demos/Hello/Hello.tpl", //template content path + isInlineLocalized: true + ) + ); + } +} +```` + +* `context` 对象用于添加新模板或获取依赖模块定义的模板. 使用 `context.Add(...)` 定义新模板. +* `TemplateDefinition` 是代表模板的类,每个模板必须有唯一的名称(在渲染模板时使用). +* `/Demos/Hello/Hello.tpl` 是模板文件的路径. +* `isInlineLocalized` 声明针对所有语言使用一个模板(`true` 还是针对每种语言使用不同的模板(`false`). 更多内容参阅下面的本地化部分. + +### 模板内容 + +`WithVirtualFilePath` 表示我们使用[虚拟文件系统](Virtual-File-System.md)存储模板内容. 在项目内创建一个 `Hello.tpl` 文件,并在属性窗口中将其标记为"**嵌入式资源**": + +![hello-template](images/hello-template.png) + +示例 `Hello.tpl` 内容如下所示: + +```` +Hello {%{{{model.name}}}%} :) +```` + +[虚拟文件系统](Virtual-File-System.md) 需要在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法添加你的文件: + +````csharp +Configure(options => +{ + options.FileSets.AddEmbedded("TextTemplateDemo"); +}); +```` + +* `TextTemplateDemoModule`是模块类. +* `TextTemplateDemo` 是你的项目的根命名空间. + +## 渲染模板 + +`ITemplateRenderer` 服务用于渲染模板内容. + +### 示例: 渲染一个简单的模板 + +````csharp +public class HelloDemo : ITransientDependency +{ + private readonly ITemplateRenderer _templateRenderer; + + public HelloDemo(ITemplateRenderer templateRenderer) + { + _templateRenderer = templateRenderer; + } + + public async Task RunAsync() + { + var result = await _templateRenderer.RenderAsync( + "Hello", //the template name + new HelloModel + { + Name = "John" + } + ); + + Console.WriteLine(result); + } +} +```` + +* `HelloDemo` 是一个简单的类,在构造函数注入了 `ITemplateRenderer` 并在 `RunAsync` 方法中使用它. +* `RenderAsync` 有两个基本参数: + * `templateName`: 要渲染的模板名称 (本示例中是 `Hello`). + * `model`: 在模板内部用做 `model` 的对象 (本示例中是 `HelloModel` 对象). + +示例会返回以下结果: + +````csharp +Hello John :) +```` + +### 匿名模型 + +虽然建议为模板创建模型类,但在简单情况下使用匿名对象也是可行的: + +````csharp +var result = await _templateRenderer.RenderAsync( + "Hello", + new + { + Name = "John" + } +); +```` + +示例中我们并没有创建模型类,但是创建了一个匿名对象模型. + +### 大驼峰 与 小驼峰 + +PascalCase 属性名(如 `UserName`) 在模板中用做小驼峰(如 `userName`). + +## 本地化 + +可以基于当前文化对模板内容进行本地化. 以下部分描述了两种类型的本地化选项. + +### 内联本地化 + +内联本地化使用[本地化系统](Localization.md)本地化模板内的文本. + +#### 示例: 重置密码链接 + +假设你需要向用户发送电子邮件重置密码. 模板内容: + +```` +{%{{{L "ResetMyPassword"}}}%} +```` + +`L` 函数用于根据当前用户的文化来定位给定的Key,你需要在本地化文件中定义 `ResetMyPassword` 键: + +````json +"ResetMyPassword": "Click here to reset your password" +```` + +你还需要在模板定义提供程序类中声明要与此模板一起使用的本地化资源: + +````csharp +context.Add( + new TemplateDefinition( + "PasswordReset", //Template name + typeof(DemoResource) //LOCALIZATION RESOURCE + ).WithVirtualFilePath( + "/Demos/PasswordReset/PasswordReset.tpl", //template content path + isInlineLocalized: true + ) +); +```` + +当你这样渲染模板时: + +````csharp +var result = await _templateRenderer.RenderAsync( + "PasswordReset", //the template name + new PasswordResetModel + { + Link = "https://abp.io/example-link?userId=123&token=ABC" + } +); +```` + +你可以看到以下本地化结果: + +````csharp +Click here to reset your password +```` + +> 如果你为应用程序定义了 [默认本地化资源](Localization.md), 则无需声明模板定义的资源类型. + +### 多个内容本地化 + +你可能希望为每种语言创建不同的模板文件,而不是使用本地化系统本地化单个模板. 如果模板对于特定的文化(而不是简单的文本本地化)应该是完全不同的,则可能需要使用它. + +#### 示例: 欢迎电子邮件模板 + +假设你要发送电子邮件欢迎用户,但要定义基于用户的文化完全不同的模板. + +首先创建一个文件夹,将模板放在里面,像 `en.tpl`, `tr.tpl` 每一个你支持的文化: + +![multiple-file-template](images/multiple-file-template.png) + +然后在模板定义提供程序类中添加模板定义: + +````csharp +context.Add( + new TemplateDefinition( + name: "WelcomeEmail", + defaultCultureName: "en" + ) + .WithVirtualFilePath( + "/Demos/WelcomeEmail/Templates", //template content folder + isInlineLocalized: false + ) +); +```` + +* 设置 **默认文化名称**, 当没有所需的文化模板,回退到缺省文化. +* 指定 **模板文件夹** 而不是单个模板文件. +* 设置 `isInlineLocalized` 为 `false`. + +就这些,你可以渲染当前文化的模板: + +````csharp +var result = await _templateRenderer.RenderAsync("WelcomeEmail"); +```` + +> 为了简单我们跳过了模型,但是你可以使用前面所述的模型. + +### 指定文化 + +`ITemplateRenderer` 服务如果没有指定则使用当前文化 (`CultureInfo.CurrentUICulture`). 如果你需要你可以使用 `cultureName` 参数指定文化. + +````csharp +var result = await _templateRenderer.RenderAsync( + "WelcomeEmail", + cultureName: "en" +); +```` + +## 布局模板 + +布局模板用于在其他模板之间创建共享布局. 它类似于ASP.NET Core MVC / Razor Pages中的布局系统. + +### 示例: 邮件HTML布局模板 + +例如,你想为所有电子邮件模板创建一个布局. + +首先像之前一样创建一个模板文件: + +````xml + + + + + + + {%{{{content}}}%} + + +```` + +* 布局模板必须具有 **{%{{{content}}}%}** 部分作为渲染的子内容的占位符. + +在模板定义提供程序中注册模板: + +````csharp +context.Add( + new TemplateDefinition( + "EmailLayout", + isLayout: true //SET isLayout! + ).WithVirtualFilePath( + "/Demos/EmailLayout/EmailLayout.tpl", + isInlineLocalized: true + ) +); +```` + +现在你可以将此模板用作任何其他模板的布局: + +````csharp +context.Add( + new TemplateDefinition( + name: "WelcomeEmail", + defaultCultureName: "en", + layout: "EmailLayout" //Set the LAYOUT + ).WithVirtualFilePath( + "/Demos/WelcomeEmail/Templates", + isInlineLocalized: false + ) +); +```` + +## 全局上下文 + +ABP传递 `model`,可用于访问模板内的模型. 如果需要,可以传递更多的全局变量. + +示例模板内容: + +```` +A global object value: {%{{{myGlobalObject}}}%} +```` + +模板假定它渲染上下文中的 `myGlobalObject` 对象. 你可以如下所示提供它: + +````csharp +var result = await _templateRenderer.RenderAsync( + "GlobalContextUsage", + globalContext: new Dictionary + { + {"myGlobalObject", "TEST VALUE"} + } +); +```` + +渲染的结果将是: + +```` +A global object value: TEST VALUE +```` + +## 高级功能 + +本节介绍文本模板系统的一些内部知识和高级用法. + +### 模板内容Provider + +`TemplateRenderer` 用于渲染模板,这是大多数情况下所需的模板. 但是你可以使用 `ITemplateContentProvider` 获取原始(未渲染的)模板内容. + +> `ITemplateRenderer` 内部使用 `ITemplateContentProvider` 获取原始模板内容. + +示例: + +````csharp +public class TemplateContentDemo : ITransientDependency +{ + private readonly ITemplateContentProvider _templateContentProvider; + + public TemplateContentDemo(ITemplateContentProvider templateContentProvider) + { + _templateContentProvider = templateContentProvider; + } + + public async Task RunAsync() + { + var result = await _templateContentProvider + .GetContentOrNullAsync("Hello"); + + Console.WriteLine(result); + } +} +```` + +结果是原始模板内容: + +```` +Hello {%{{{model.name}}}%} :) +```` + +* `GetContentOrNullAsync` 如果没有为请求的模板定义任何内容,则返回 `null`. +* 它可以获取 `cultureName` 参数,如果模板针对不同的文化具有不同的文件,则可以使用该参数(请参见上面的"多内容本地化"部分). + +### 模板内容贡献者 + +`ITemplateContentProvider` 服务使用 `ITemplateContentContributor` 实现来查找模板内容. 有一个预实现的内容贡献者 `VirtualFileTemplateContentContributor`,它从上面描述的虚拟文件系统中获取模板内容. + +你可以实现 `ITemplateContentContributor` 从另一个源读取原始模板内容. + +示例: + +````csharp +public class MyTemplateContentProvider + : ITemplateContentContributor, ITransientDependency +{ + public async Task GetOrNullAsync(TemplateContentContributorContext context) + { + var templateName = context.TemplateDefinition.Name; + + //TODO: Try to find content from another source + return null; + } +} + +```` + +如果源无法找到内容, 则返回 `null`, `ITemplateContentProvider` 将回退到下一个贡献者. + +### Template Definition Manager + +`ITemplateDefinitionManager` 服务可用于获取模板定义(由模板定义提供程序创建). + +## 另请参阅 + +* 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). +* [本地化系统](Localization.md). +* [虚拟文件系统](Virtual-File-System.md). \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Angular/Part-I.md b/docs/zh-Hans/Tutorials/Angular/Part-I.md index 8e8c74ec59..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/Angular/Part-I.md +++ b/docs/zh-Hans/Tutorials/Angular/Part-I.md @@ -1,3 +1,8 @@ -## Angular 教程 - 第一章 +# 教程 -TODO... \ No newline at end of file +## 应用程序开发 + +* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [Angular UI](../Part-1?UI=NG) + + \ 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 7c2db90997..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md @@ -1,478 +1,8 @@ -## ASP.NET Core MVC 介绍 - 第一章 +# 教程 -### 关于本教程 +## 应用程序开发 -在本系列教程中, 你将构建一个用于管理书籍及其作者列表的应用程序. **Entity Framework Core**(EF Core)将用作ORM提供者,因为它是默认数据库提供者. +* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [Angular UI](../Part-1?UI=NG) -这是本教程所有章节中的第一章,下面是所有的章节: - -- **Part I: 创建项目和书籍列表页面(本章)** -- [Part II: 创建,编辑,删除书籍](Part-II.md) -- [Part III: 集成测试](Part-III.md) - -你可以从[GitHub存储库](https://github.com/abpframework/abp-samples/tree/master/BookStore)访问应用程序的**源代码**. - -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). - -### 创建项目 - -创建一个名为`Acme.BookStore`的新项目, 创建数据库并按照[入门文档](../../../Getting-Started-AspNetCore-MVC-Template.md)运行应用程序. - -### 解决方案的结构 - -下面的图片展示了从启动模板创建的项目是如何分层的. - -![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.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中 - -EF Core需要你将实体和DbContext建立关联.最简单的做法是在`Acme.BookStore.EntityFrameworkCore`项目的`BookStoreDbContext`类中添加`DbSet`属性.如下所示: - -````C# -public class BookStoreDbContext : AbpDbContext -{ - public DbSet Books { get; set; } - ... -} -```` - -#### 配置你的Book实体 - -在`Acme.BookStore.EntityFrameworkCore`项目中打开`BookStoreDbContextModelCreatingExtensions.cs`文件,并将以下代码添加到`ConfigureBookStore`方法的末尾以配置Book实体: - -````C# -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); -}); -```` - -#### 添加新的Migration并更新数据库 - -这个启动模板使用了[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)来创建并维护数据库结构.打开 **程序包管理器控制台(Package Manager Console) (PMC)** (工具/Nuget包管理器菜单),选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令: - -![bookstore-pmc-add-book-migration](images/bookstore-pmc-add-book-migration-v2.png) - -这样就会在`Migrations`文件夹中创建一个新的migration类.然后执行`Update-Database`命令更新数据库结构. - -```` -PM> Update-Database -```` - -#### 添加示例数据 - -`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: - -![bookstore-books-table](images/bookstore-books-table.png) - -### 创建应用服务 - -下一步是创建[应用服务](../../../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 -CreateMap(); -```` - -#### 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来获取书籍列表. - -### 动态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); }); -```` - -你会看到控制台会显示类似这样的输出: - -```` -successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7 -```` - -检查数据库中的`Books`表以查看新书. 你可以自己尝试`get`,`update`和`delete`功能. - -### 创建书籍页面 - -现在我们来创建一些可见和可用的东西,取代经典的MVC,我们使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start). - - -在 `Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Books`并添加一个名为`Index.cshtml`的Razor Page. - -![bookstore-add-index-page](images/bookstore-add-index-page-v2.png) - -打开`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`中更新它. - -#### 将Books页面添加到主菜单 - -打开`Menus`文件夹中的 `BookStoreMenuContributor` 类,在`ConfigureMainMenuAsync`方法的底部添加如下代码: - -````c# -context.Menu.AddItem( - new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) - .AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")) -); -```` - -#### 本地化菜单 - -本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下: - -![bookstore-localization-files](images/bookstore-localization-files-v2.png) - -打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾: - -````json -{ - "culture": "en", - "texts": { - "Menu:BookStore": "Book Store", - "Menu:Books": "Books" - } -} -```` - -* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](https://docs.abp.io/zh-Hans/abp/latest/Localization). -* 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加`Menu:`前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将**返回**到本地化的key(ASP.NET Core的标准行为). - -运行该应用程序,看到新菜单项已添加到顶部栏: - -![bookstore-menu-items](images/bookstore-menu-items.png) - -点击Books菜单项就会跳转到新增的书籍页面. - -#### 书籍列表 - -我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. 数据表可以完全通过AJAX工作,速度快,并提供良好的用户体验. Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,而需要在页面中引用样式和脚本文件. - -##### Index.cshtml - -将`Pages/Books/Index.cshtml`改成下面的样子: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@model Acme.BookStore.Web.Pages.Books.IndexModel -@section scripts -{ - -} - - -

@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的格式相适应的辅助方法. -* `abp.libs.datatables.normalizeConfiguration`是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置. -* `acme.bookStore.book.getList`是获取书籍列表的方法(上面已经介绍过了) -* 查看 [Datatable文档](https://datatables.net/manual/) 了解更多配置项. - -最终的页面如下: - -![bookstore-book-list](images/bookstore-book-list-2.png) - -### 下一章 - -点击查看 [下一章](Part-II.md) 的介绍. + \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md index 2d4b344afa..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md @@ -1,432 +1,8 @@ -## ASP.NET Core MVC 教程 - 第二章 +# 教程 -### 关于本教程 +## 应用程序开发 -这是ASP.NET Core MVC教程系列的第二章. 查看其它章节 +* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [Angular UI](../Part-1?UI=NG) -* [Part I: 创建项目和书籍列表页面](Part-I.md) -* **Part II: 创建,编辑,删除书籍(本章)** -* [Part III: 集成测试](Part-III.md) - -你可以从[GitHub存储库](https://github.com/volosoft/abp/tree/master/samples/BookStore)访问应用程序的**源代码**. - -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). - -### 新增 Book 实体 - -通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: - -![bookstore-create-dialog](images/bookstore-create-dialog-2.png) - -#### 新建 modal form - -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: - -![bookstore-add-create-dialog](images/bookstore-add-create-dialog-v2.png) - -##### CreateModal.cshtml.cs - -展开 `CreateModal.cshtml`,打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: - -````C# -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task 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实体. - -### 下一章 - -查看本教程的 [下一章](Part-III.md) . + \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md index e072e5a1a7..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md @@ -1,165 +1,8 @@ -## ASP.NET Core MVC 教程 - 第三章 +# 教程 -### 关于本教程 +## 应用程序开发 -这是ASP.NET Core MVC教程系列的第三章. 查看其它章节 +* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [Angular UI](../Part-1?UI=NG) -- [Part I: 创建项目和书籍列表页面](Part-I.md) -- [Part II: 创建,编辑,删除书籍](Part-II.md) -- **Part III: 集成测试(本章)** - -你可以从[GitHub存储库](https://github.com/volosoft/abp/tree/master/samples/BookStore)访问应用程序的**源代码**. - -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). - -### 解决方案中的测试项目 - -解决方案中有多个测试项目: - -![bookstore-test-projects-v2](images/bookstore-test-projects-v2.png) - -每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: - -* [xunit](https://xunit.github.io/) 作为主测试框架. -* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. -* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. - -### 添加测试用数据 - -启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试. -更改`BookStoreTestDataSeedContributor`类如下所示: - -````C# -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _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(_guidGenerator.Create(), "Test book 1", - BookType.Fantastic, new DateTime(2015, 05, 24), 21)); - - await _bookRepository.InsertAsync( - new Book(_guidGenerator.Create(), "Test book 2", - BookType.Science, new DateTime(2014, 02, 11), 15)); - } - } -} -```` - -* 注入`IRepository`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. -* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息). - -### 测试 BookAppService - -在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: - -````C# -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Xunit; - -namespace Acme.BookStore -{ - public class BookAppService_Tests : BookStoreApplicationTestBase - { - private readonly IBookAppService _bookAppService; - - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "Test book 1"); - } - } -} -```` - -* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. - -新增测试方法,用以测试创建一个合法book实体的场景: - -````C# -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -```` - -新增测试方法,用以测试创建一个非法book实体失败的场景: - -````C# -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(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) - -恭喜, 绿色图标表示测试已成功通过! + \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md index 43590faf2b..9589c430b6 100644 --- a/docs/zh-Hans/Tutorials/Part-1.md +++ b/docs/zh-Hans/Tutorials/Part-1.md @@ -30,7 +30,7 @@ ASP.NET Core {{UI_Value}} 系列教程包括三个3个部分: - [Part-2: 创建,编辑,删除书籍](Part-2.md) - [Part-3: 集成测试](Part-3.md) -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). +> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-booktore-application). ### 创建新项目 @@ -44,19 +44,19 @@ ASP.NET Core {{UI_Value}} 系列教程包括三个3个部分: abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none ``` -![Creating project](./images/bookstore-create-project-{{UI_Text}}.png) +![Creating project](./images/booktore-create-project-{{UI_Text}}.png) ### 应用迁移 项目创建后,需要应用初始化迁移创建数据库. 运行 `Acme.BookStore.DbMigrator` 应用程序. 它会应用所有迁移,完成流程后你会看到以下结果,数据库已经准备好了! -![Migrations applied](./images/bookstore-migrations-applied-{{UI_Text}}.png) +![Migrations applied](./images/booktore-migrations-applied-{{UI_Text}}.png) > 另外你也可以在 Visual Studio 包管理控制台运行 `Update-Database` 命令应用迁移. #### 初始化数据库表 -![Initial database tables](./images/bookstore-database-tables-{{DB}}.png) +![Initial database tables](./images/booktore-database-tables-{{DB}}.png) ### 运行应用程序 @@ -64,7 +64,7 @@ abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text} 更多信息,参阅[入门教程](../../Getting-Started?UI={{UI}})的运行应用程序部分. -![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png) +![Set as startup project](./images/booktore-start-project-{{UI_Text}}.png) {{if UI == "NG"}} @@ -105,7 +105,7 @@ http://localhost:4200/ 下面的图片展示了从启动模板创建的项目是如何分层的. -![bookstore-visual-studio-solution](./images/bookstore-solution-structure-{{UI_Text}}.png) +![booktore-visual-studio-solution](./images/booktore-solution-structure-{{UI_Text}}.png) > 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构. @@ -185,7 +185,7 @@ EF Core需要你将实体和 `DbContext` 建立关联.最简单的做法是在`A ````C# public class BookStoreDbContext : AbpDbContext { - public DbSet Books { get; set; } + public DbSet Book { get; set; } ... } ```` @@ -194,13 +194,13 @@ public class BookStoreDbContext : AbpDbContext {{if DB == "mongodb"}} -添加 `IMongoCollection Books` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中. +添加 `IMongoCollection Book` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中. ```csharp public class BookStoreMongoDbContext : AbpMongoDbContext { public IMongoCollection Users => Collection(); - public IMongoCollection Books => Collection();//<--added this line--> + public IMongoCollection Book => Collection();//<--added this line--> //... } ``` @@ -216,7 +216,7 @@ public class BookStoreMongoDbContext : AbpMongoDbContext ````csharp builder.Entity(b => { - b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); + b.ToTable(BookStoreConsts.DbTablePrefix + "Book", BookStoreConsts.DbSchema); b.ConfigureByConvention(); //auto configure for the base class props b.Property(x => x.Name).IsRequired().HasMaxLength(128); }); @@ -295,7 +295,7 @@ namespace Acme.BookStore 这个启动模板使用了[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) +![Open Package Manager Console](./images/booktore-open-package-manager-console.png) 选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令: @@ -303,7 +303,7 @@ namespace Acme.BookStore Add-Migration "Created_Book_Entity" ``` -![bookstore-pmc-add-book-migration](./images/bookstore-pmc-add-book-migration-v2.png) +![booktore-pmc-add-book-migration](./images/booktore-pmc-add-book-migration-v2.png) 这样就会在 `Migrations` 文件夹中创建一个新的migration类.然后执行 `Update-Database` 命令更新数据库结构: @@ -311,24 +311,24 @@ Add-Migration "Created_Book_Entity" Update-Database ```` -![bookstore-update-database-after-book-entity](./images/bookstore-update-database-after-book-entity.png) +![booktore-update-database-after-book-entity](./images/booktore-update-database-after-book-entity.png) #### 添加示例数据 -`Update-Database`命令在数据库中创建了`AppBooks`表. 打开数据库并输入几个示例行,以便在页面上显示它们: +`Update-Database`命令在数据库中创建了`AppBook`表. 打开数据库并输入几个示例行,以便在页面上显示它们: ```mssql -INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +INSERT INTO AppBook (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 +INSERT INTO AppBook (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 +INSERT INTO AppBook (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) +![booktore-book-table](./images/booktore-book-table.png) {{end}} @@ -501,7 +501,7 @@ namespace Acme.BookStore 你会看到一些内置的接口和`Book`的接口,它们都是REST风格的: -![bookstore-swagger](images/bookstore-swagger.png) +![booktore-swagger](images/booktore-swagger.png) Swagger有一个很好的UI来测试API. 你可以尝试执行`[GET] /api/app/book` API来获取书籍列表. @@ -529,11 +529,11 @@ acme.bookStore.book.getList({}).done(function (result) { console.log(result); }) 运行这段代码会产生下面的输出: -![bookstore-test-js-proxy-getlist](./images/bookstore-test-js-proxy-getlist.png) +![booktore-test-js-proxy-getlist](./images/booktore-test-js-proxy-getlist.png) 你可以看到服务器返回的 **book list**.你还可以切换到开发者工具的 **network** 查看客户端到服务器端的通讯信息: -![bookstore-test-js-proxy-getlist-network](./images/bookstore-test-js-proxy-getlist-network.png) +![booktore-test-js-proxy-getlist-network](./images/booktore-test-js-proxy-getlist-network.png) 我们使用`create`方法 **创建一本新书**: @@ -547,15 +547,15 @@ acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05- successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 ```` -检查数据库中的`Books`表以查看新书. 你可以自己尝试`get`,`update`和`delete`功能. +检查数据库中的`Book`表以查看新书. 你可以自己尝试`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. +在 `Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Book`并添加一个名为`Index.cshtml`的Razor Page. -![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) +![booktore-add-index-page](./images/booktore-add-index-page-v2.png) 打开`Index.cshtml`并把内容修改成下面这样: @@ -563,22 +563,22 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 ````html @page -@using Acme.BookStore.Web.Pages.Books +@using Acme.BookStore.Web.Pages.Book @inherits Acme.BookStore.Web.Pages.BookStorePage @model IndexModel -

Books

+

Book

```` * 此代码更改了Razor View Page Model的默认继承,因此它从`BookStorePage`类(而不是`PageModel`)继承.启动模板附带的`BookStorePage`类,提供所有页面使用的一些共享属性/方法. -* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Books`命名空间,或者在`Index.cshtml`中更新它. +* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Book`命名空间,或者在`Index.cshtml`中更新它. **Index.cshtml.cs:** ```csharp using Microsoft.AspNetCore.Mvc.RazorPages; -namespace Acme.BookStore.Web.Pages.Books +namespace Acme.BookStore.Web.Pages.Book { public class IndexModel : PageModel { @@ -590,7 +590,7 @@ namespace Acme.BookStore.Web.Pages.Books } ``` -#### 将Books页面添加到主菜单 +#### 将Book页面添加到主菜单 打开`Menus`文件夹中的 `BookStoreMenuContributor` 类,在`ConfigureMainMenuAsync`方法的底部添加如下代码: @@ -604,9 +604,9 @@ namespace Acme.BookStore.Web.Menus { //<-- added the below code context.Menu.AddItem( - new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) + new ApplicationMenuItem("BookStore", l["Menu:BookStore"]) .AddItem( - new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books") + new ApplicationMenuItem("BookStore.Book", l["Menu:Book"], url: "/Book") ) ); //--> @@ -621,9 +621,9 @@ namespace Acme.BookStore.Web.Menus 本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下: -![bookstore-localization-files](./images/bookstore-localization-files-v2.png) +![booktore-localization-files](./images/booktore-localization-files-v2.png) -打开`en.json`文件,将`Menu:BookStore`和`Menu:Books`键的本地化文本添加到文件末尾: +打开`en.json`文件,将`Menu:BookStore`和`Menu:Book`键的本地化文本添加到文件末尾: ````json { @@ -634,7 +634,7 @@ namespace Acme.BookStore.Web.Menus "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", + "Menu:Book": "Book", "Actions": "Actions", "Edit": "Edit", "PublishDate": "Publish date", @@ -653,9 +653,9 @@ namespace Acme.BookStore.Web.Menus 运行该应用程序,看到新菜单项已添加到顶部栏: -![bookstore-menu-items](./images/bookstore-new-menu-item.png) +![booktore-menu-items](./images/booktore-new-menu-item.png) -点击BookStore下Books子菜单项就会跳转到新增的书籍页面. +点击BookStore下Book子菜单项就会跳转到新增的书籍页面. #### 书籍列表 @@ -663,22 +663,22 @@ namespace Acme.BookStore.Web.Menus ##### Index.cshtml -将`Pages/Books/Index.cshtml`改成下面的样子: +将`Pages/Book/Index.cshtml`改成下面的样子: ````html @page @inherits Acme.BookStore.Web.Pages.BookStorePage -@model Acme.BookStore.Web.Pages.Books.IndexModel +@model Acme.BookStore.Web.Pages.Book.IndexModel @section scripts { - + } -

@L["Books"]

+

@L["Book"]

- + @L["Name"] @@ -699,15 +699,15 @@ namespace Acme.BookStore.Web.Menus #### 添加脚本文件 -在`Pages/Books/`文件夹中创建 `index.js`文件 +在`Pages/Book/`文件夹中创建 `index.js`文件 -![bookstore-index-js-file](./images/bookstore-index-js-file-v2.png) +![booktore-index-js-file](./images/booktore-index-js-file-v2.png) `index.js`的内容如下: ````js $(function () { - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + var dataTable = $('#BookTable').DataTable(abp.libs.datatables.normalizeConfiguration({ ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), columnDefs: [ { data: "name" }, @@ -727,7 +727,7 @@ $(function () { 最终的页面如下: -![Book list](./images/bookstore-book-list-2.png) +![Book list](./images/booktore-book-list-2.png) {{end}} @@ -735,7 +735,7 @@ $(function () { ### Angular 开发 -#### 创建books页面 +#### 创建book页面 是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具: @@ -752,31 +752,31 @@ $(function () { yarn ``` -#### BooksModule +#### BookModule -运行以下命令创建一个名为 `BooksModule` 的新模块: +运行以下命令创建一个名为 `BookModule` 的新模块: ```bash -yarn ng generate module books --route books --module app.module +yarn ng generate module book --routing true ``` -![Generating books module](./images/bookstore-creating-books-module-terminal.png) +![Generating book module](./images/booktore-creating-book-module-terminal.png) #### 路由 -打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的 `import` 和替换 `books` 路径: +打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的 `import` 和路由: ```js import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== -//...replaced original books path with the below +//...added book path with the below to the routes array { - path: 'books', + path: 'book', component: ApplicationLayoutComponent, - loadChildren: () => import('./books/books.module').then(m => m.BooksModule), + loadChildren: () => import('./book/book.module').then(m => m.BookModule), data: { routes: { - name: '::Menu:Books', + name: '::Menu:Book', iconClass: 'fas fa-book' } as ABP.Route }, @@ -785,71 +785,50 @@ import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added th * `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 +yarn ng generate component book/book-list ``` -![Creating books list](./images/bookstore-creating-book-list-terminal.png) +![Creating book list](./images/booktore-creating-book-list-terminal.png) -打开 `app\books` 目录下的 `books.module.ts` 文件,使用以下内容替换它: +打开 `app\book` 目录下的 `book.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 { BookRoutingModule } from './book-routing.module'; import { BookListComponent } from './book-list/book-list.component'; import { SharedModule } from '../shared/shared.module'; //<== added this line ==> @NgModule({ - declarations: [BooksComponent, BookListComponent], + declarations: [BookListComponent], imports: [ CommonModule, - BooksRoutingModule, + BookRoutingModule, SharedModule, //<== added this line ==> - ] + ], }) -export class BooksModule { } +export class BookModule {} ``` * 我们导入了 `SharedModule` 并添加到 `imports` 数组. -打开 `app\books` 目录下的 `books-routing.module.ts` 文件用以下内容替换它: +打开 `app\book` 目录下的 `book-routing.module.ts` 文件用以下内容替换它: ```js import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookListComponent } from './book-list/book-list.component'; // <== added this line ==> -import { BooksComponent } from './books.component'; -import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==> - -//<== replaced routes ==> +// <== replaced routes ==> const routes: Routes = [ { path: '', - component: BooksComponent, - children: [{ path: '', component: BookListComponent }], + component: BookListComponent, }, ]; @@ -857,36 +836,42 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) -export class BooksRoutingModule { } +export class BookRoutingModule { } ``` * 我们导入了 `BookListComponent` 并替换 `routes` 常量. -我们将看到books页面的 **book-list works!**: +运行 `yarn start`,等待Angular启动服务: + +```bash +yarn start +``` + +我们将看到book页面的 **book-list works!**: -![Initial book list page](./images/bookstore-initial-book-list-page.png) +![Initial book list page](./images/booktore-initial-book-list-page.png) -#### 创建 BooksState +#### 创建 BookState -运行以下命令创建名为 `BooksState` 的新state: +运行以下命令创建名为 `BookState` 的新state: ```bash -npx @ngxs/cli --name books --directory src/app/books +npx @ngxs/cli --name book --directory src/app/book ``` -* 此命令在 `src/app/books/state` 文件夹下创建了 `books.state.ts` 和 `books.actions.ts` 文件. 参阅 [NGXS CLI文档](https://www.ngxs.io/plugins/cli)了解更多. +* 此命令在 `src/app/book/state` 文件夹下创建了 `book.state.ts` 和 `book.actions.ts` 文件. 参阅 [NGXS CLI文档](https://www.ngxs.io/plugins/cli)了解更多. -将 `BooksState` 导入到 `src/app` 文件夹中的 `app.module.ts` 中. 然后添加 `BooksState` 到 `NgxsModule` 的 `forRoot` 静态方法,作为该方法的第一个参数的数组元素. +将 `BookState` 导入到 `src/app` 文件夹中的 `app.module.ts` 中. 然后添加 `BookState` 到 `NgxsModule` 的 `forRoot` 静态方法,作为该方法的第一个参数的数组元素. ```js // ... -import { BooksState } from './books/state/books.state'; //<== imported BooksState ==> +import { BookState } from './book/state/book.state'; //<== imported BookState ==> @NgModule({ imports: [ // other imports - NgxsModule.forRoot([BooksState]), //<== added BooksState ==> + NgxsModule.forRoot([BookState]), //<== added BookState ==> //other imports ], @@ -911,50 +896,50 @@ abp generate-proxy --module app ![Generated files](./images/generated-proxies.png) -#### GetBooks 动作 +#### GetBook 动作 动作可以被认为是一个命令,它应该触发某些事情发生,或者是已经发生的事情的结果事件.[See NGXS Actions文档](https://www.ngxs.io/concepts/actions). -打开 `app/books/state` 目录下的 `books.actions.ts` 文件用以下内容替换它: +打开 `app/book/state` 目录下的 `book.actions.ts` 文件用以下内容替换它: ```js -export class GetBooks { - static readonly type = '[Books] Get'; +export class GetBook { + static readonly type = '[Book] Get'; } ``` -#### 实现 BooksState +#### 实现 BookState -打开 `app/books/state` 目录下的 `books.state.ts` 文件用以下内容替换它: +打开 `app/book/state` 目录下的 `book.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 { GetBooks } from './book.actions'; +import { BookService } from '../services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { BookDto } from '../../app/shared/models'; +import { BookDto } from '../models'; -export class BooksStateModel { +export class BookStateModel { public book: PagedResultDto; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + name: 'BookState', + defaults: { book: {} } as BookStateModel, }) @Injectable() -export class BooksState { +export class BookState { @Selector() - static getBooks(state: BooksStateModel) { + static getBooks(state: BookStateModel) { return state.book.items || []; } constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { + get(ctx: StateContext) { return this.bookService.getListByInput().pipe( tap((booksResponse) => { ctx.patchState({ @@ -966,22 +951,22 @@ export class BooksState { } ``` -* 我们添加了book属性到BooksStateModel模态框. -* 我们添加了 `GetBooks` 动作. 它通过 ABP CLI生成的 `BooksService` 检索图书数据. +* 我们添加了book属性到BookStateModel模态框. +* 我们添加了 `GetBook` 动作. 它通过 ABP CLI生成的 `BookService` 检索图书数据. * `NGXS` 需要在不订阅get函数的情况下返回被观察对象. #### BookListComponent -打开 `app\books\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它: +打开 `app\book\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks } from '../state/book.actions'; +import { BookState } from '../state/book.state'; @Component({ selector: 'app-book-list', @@ -989,7 +974,7 @@ import { BooksState } from '../state/books.state'; styleUrls: ['./book-list.component.scss'], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -1012,10 +997,10 @@ export class BookListComponent implements OnInit { } ``` -* 我们添加了 `get` 函数获取books更新store. +* 我们添加了 `get` 函数获取book更新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` 用以下内容替换它: +打开 `app\book\book-list` 目录下的 `book-list.component.html` 用以下内容替换它: ```html
@@ -1023,7 +1008,7 @@ export class BookListComponent implements OnInit {
- {%{{{ "::Menu:Books" | abpLocalization }}}%} + {%{{{ "::Menu:Book" | abpLocalization }}}%}
@@ -1031,7 +1016,7 @@ export class BookListComponent implements OnInit {
{%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} + {%{{{ bookType[data.type] }}}%} {%{{{ data.publishDate | date }}}%} {%{{{ data.price }}}%} @@ -1063,11 +1048,11 @@ export class BookListComponent implements OnInit { 现在你可以在浏览器看到最终结果: -![Book list final result](./images/bookstore-book-list.png) +![Book list final result](./images/booktore-book-list.png) 项目的文件系统结构: -![Book list final result](./images/bookstore-angular-file-tree.png) +![Book list final result](./images/booktore-angular-file-tree.png) 在本教程中我们遵循了官方的[Angular风格指南](https://angular.io/guide/styleguide#file-tree). diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md index 56dadcbbcd..5e73c753e6 100644 --- a/docs/zh-Hans/Tutorials/Part-2.md +++ b/docs/zh-Hans/Tutorials/Part-2.md @@ -29,7 +29,7 @@ end - **Part 2: 创建,编辑,删除书籍(本章)** - [Part-3: 集成测试](Part-3.md) -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). +> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-booktore-application). {{if UI == "MVC"}} @@ -37,13 +37,13 @@ end 通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: -![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) +![booktore-create-dialog](./images/booktore-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) +![booktore-add-create-dialog](./images/booktore-add-create-dialog-v2.png) ##### CreateModal.cshtml.cs @@ -130,9 +130,9 @@ namespace Acme.BookStore.Web.Pages.Books 如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: -![bookstore-new-book-button](./images/bookstore-new-book-button.png) +![booktore-new-book-button](./images/booktore-new-book-button.png) -打开 `Pages/books/index.js` 在 `datatable` 配置代码后面添加如下代码: +打开 `Pages/book/index.js` 在 `datatable` 配置代码后面添加如下代码: ````js var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); @@ -155,7 +155,7 @@ $('#NewBookButton').click(function (e) { 在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: -![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) +![booktore-add-edit-dialog](./images/booktore-add-edit-dialog.png) #### EditModal.cshtml.cs @@ -258,7 +258,7 @@ namespace Acme.BookStore.Web 我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下: -![bookstore-books-table-actions](images/bookstore-books-table-actions.png) +![booktore-book-table-actions](images/booktore-book-table-actions.png) 打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: @@ -279,7 +279,7 @@ namespace Acme.BookStore.Web * 只是为"Actions"增加了一个 `th` 标签. -打开 `Pages/books/index.js` 并用以下内容进行替换: +打开 `Pages/book/index.js` 并用以下内容进行替换: ````js $(function () { @@ -346,7 +346,7 @@ $(function () { ### 删除一个已有的Book实体 -打开 `Pages/books/index.js` 文件,在 `rowAction` `items` 下新增一项: +打开 `Pages/book/index.js` 文件,在 `rowAction` `items` 下新增一项: ````js { @@ -456,54 +456,54 @@ $(function () { #### 状态定义 -在 `books\state` 文件夹下打开 `books.action.ts` 文件,使用以下内容替换它: +在 `app\book\state` 文件夹下打开 `book.action.ts` 文件,使用以下内容替换它: ```js -import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==> +import { CreateUpdateBookDto } from '../models'; //<== added this line ==> export class GetBooks { - static readonly type = '[Books] Get'; + static readonly type = '[Book] Get'; } // added CreateUpdateBook class export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; + static readonly type = '[Book] Create Update Book'; constructor(public payload: CreateUpdateBookDto) { } } ``` * 我们导入了 `CreateUpdateBookDto` 模型并且创建了 `CreateUpdateBook` 动作. -打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换它: +打开 `app\book\state` 文件夹下的 `book.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 { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==> +import { BookService } from '../services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { BookDto } from '../../app/shared/models'; +import { BookDto } from '../models'; -export class BooksStateModel { +export class BookStateModel { public book: PagedResultDto; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + name: 'BookState', + defaults: { book: {} } as BookStateModel, }) @Injectable() -export class BooksState { +export class BookState { @Selector() - static getBooks(state: BooksStateModel) { + static getBooks(state: BookStateModel) { return state.book.items || []; } constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { + get(ctx: StateContext) { return this.bookService.getListByInput().pipe( tap((bookResponse) => { ctx.patchState({ @@ -515,7 +515,7 @@ export class BooksState { // added CreateUpdateBook action listener @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { + save(ctx: StateContext, action: CreateUpdateBook) { return this.bookService.createByInput(action.payload); } } @@ -527,7 +527,7 @@ export class BooksState { #### 添加模态到 BookListComponent -打开 `books\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它: +打开 `app\book\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它: ```html
@@ -556,7 +556,7 @@ export class BooksState {
{%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} + {%{{{ bookType[data.type] }}}%} {%{{{ data.publishDate | date }}}%} {%{{{ data.price }}}%} @@ -603,16 +603,16 @@ export class BooksState { * `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处. * 我们添加了 `New book` 按钮到 `AbpContentToolbar`. -打开 `books\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: +打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks } from '../state/book.actions'; +import { BookState } from '../state/book.state'; @Component({ selector: 'app-book-list', @@ -620,7 +620,7 @@ import { BooksState } from '../state/books.state'; styleUrls: ['./book-list.component.scss'], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -654,22 +654,22 @@ export class BookListComponent implements OnInit { 你可以打开浏览器,点击**New book**按钮看到模态框. -![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) +![Empty modal for new book](./images/booktore-empty-new-book-modal.png) #### 添加响应式表单 [响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入. -打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks } from '../state/book.actions'; +import { BookState } from '../state/book.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> @Component({ @@ -678,7 +678,7 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== adde styleUrls: ['./book-list.component.scss'], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -729,7 +729,7 @@ export class BookListComponent implements OnInit { #### 创建表单的DOM元素 -打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 ` `: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 ` `: ```html @@ -748,7 +748,7 @@ export class BookListComponent implements OnInit { *
@@ -772,14 +772,14 @@ export class BookListComponent implements OnInit { #### Datepicker 要求 -打开 `app\books` 文件夹下的 `books.module.ts` 文件,使用以下内容替换它: +打开 `app\book` 文件夹下的 `book.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 { BooksRoutingModule } from './book-routing.module'; +import { BooksComponent } from './book.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 ==> @@ -798,16 +798,16 @@ export class BooksModule { } * 我们导入了 `NgbDatepickerModule` 来使用日期选择器. -打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks } from '../state/book.actions'; +import { BookState } from '../state/book.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==> @@ -818,7 +818,7 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==> }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -878,20 +878,20 @@ export class BookListComponent implements OnInit { 现在你可以打开浏览器看到以下变化: -![New book modal](./images/bookstore-new-book-form.png) +![New book modal](./images/booktore-new-book-form.png) #### 保存图书 -打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks, CreateUpdateBook } from '../state/books.actions'; // <== added CreateUpdateBook ==> -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==> +import { BookState } from '../state/book.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; @@ -902,12 +902,11 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; - //added bookTypeArr array bookTypeArr = Object.keys(BookType).filter( (bookType) => typeof this.booksType[bookType] === 'number' ); @@ -946,7 +945,7 @@ export class BookListComponent implements OnInit { }); } - //<== added save ==> + // <== added save ==> save() { if (this.form.invalid) { return; @@ -964,7 +963,7 @@ export class BookListComponent implements OnInit { * 我们导入了 `CreateUpdateBook`. * 我们添加了 `save` 方法. -打开 `app\books\book-list` 文件夹下的 `app\books\book-list`文件, 添加 `abp-button` 保存图书. +打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list`文件, 添加 `abp-button` 保存图书. ```html @@ -991,34 +990,34 @@ export class BookListComponent implements OnInit { 模态框最终看起来像这样: -![Save button to the modal](./images/bookstore-new-book-form-v2.png) +![Save button to the modal](./images/booktore-new-book-form-v2.png) ### 更新图书 #### CreateUpdateBook 动作 -打开 `books\state` 文件夹下的 `books.actions.ts` 文件,使用以下内容替换它: +打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件,使用以下内容替换它: ```js -import { CreateUpdateBookDto } from '../../app/shared/models'; +import { CreateUpdateBookDto } from '../models'; export class GetBooks { - static readonly type = '[Books] Get'; + static readonly type = '[Book] Get'; } export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; - constructor(public payload: CreateUpdateBookDto, public id?: string) { } // <== added id parameter ==> + static readonly type = '[Book] Create Update Book'; + constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==> } ``` * 我们在 `CreateUpdateBook` 动作的构造函数添加了 `id` 参数. -打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换 `save` 方法: +打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换 `save` 方法: ```js @Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { +save(ctx: StateContext, action: CreateUpdateBook) { if (action.id) { return this.bookService.updateByIdAndInput(action.payload, action.id); } else { @@ -1029,19 +1028,19 @@ save(ctx: StateContext, action: CreateUpdateBook) { #### BookListComponent -打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,在构造函数注入 `BookService` 服务,并添加 名为 `selectedBook` 的变量. +打开 `app\app\book\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 { BookDto, BookType } from '../models'; +import { GetBooks, CreateUpdateBook } from '../state/book.actions'; +import { BookState } from '../state/book.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { BookService } from '../../app/shared/services'; // <== imported BookService ==> +import { BookService } from '../services'; // <== imported BookService ==> @Component({ selector: 'app-book-list', @@ -1050,13 +1049,13 @@ import { BookService } from '../../app/shared/services'; // <== imported BookSer providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) - books$: Observable; + @Select(BookState.getBooks) + book$: Observable; - booksType = BookType; + bookType = BookType; bookTypeArr = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' + (bookType) => typeof this.bookType[bookType] === 'number' ); loading = false; @@ -1137,12 +1136,12 @@ export class BookListComponent implements OnInit { #### 添加 "Actions" 下拉框到表格 -打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `
` 标签: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `
` 标签: ```html
{%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} + {%{{{ bookType[data.type] }}}%} {%{{{ data.publishDate | date }}}%} {%{{{ data.price }}}%} @@ -1193,9 +1192,9 @@ export class BookListComponent implements OnInit { UI最终看起来像这样: -![Action buttons](./images/bookstore-actions-buttons.png) +![Action buttons](./images/booktore-actions-buttons.png) -打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `` 标签: +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `` 标签: ```html @@ -1209,45 +1208,45 @@ UI最终看起来像这样: #### DeleteBook 动作 -打开 `books\state` 文件夹下的 `books.actions.ts` 文件添加名为 `DeleteBook` 的动作. +打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件添加名为 `DeleteBook` 的动作. ```js export class DeleteBook { - static readonly type = '[Books] Delete'; + static readonly type = '[Book] Delete'; constructor(public id: string) {} } ``` -打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换它: +打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它: ```js import { PagedResultDto } from '@abp/ng.core'; import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==> -import { BookService } from '../../app/shared/services'; +import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==> +import { BookService } from '../services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { BookDto } from '../../app/shared/models'; +import { BookDto } from '../models'; -export class BooksStateModel { +export class BookStateModel { public book: PagedResultDto; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + name: 'BookState', + defaults: { book: {} } as BookStateModel, }) @Injectable() -export class BooksState { +export class BookState { @Selector() - static getBooks(state: BooksStateModel) { + static getBooks(state: BookStateModel) { return state.book.items || []; } constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { + get(ctx: StateContext) { return this.bookService.getListByInput().pipe( tap((booksResponse) => { ctx.patchState({ @@ -1258,7 +1257,7 @@ export class BooksState { } @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { + save(ctx: StateContext, action: CreateUpdateBook) { if (action.id) { return this.bookService.updateByIdAndInput(action.payload, action.id); } else { @@ -1268,7 +1267,7 @@ export class BooksState { // <== added DeleteBook action listener ==> @Action(DeleteBook) - delete(ctx: StateContext, action: DeleteBook) { + delete(ctx: StateContext, action: DeleteBook) { return this.bookService.deleteById(action.id); } } @@ -1280,7 +1279,7 @@ export class BooksState { #### 删除确认弹层 -打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`. +打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`. 替换构造函数: @@ -1304,7 +1303,7 @@ constructor( 在 `book-list.component.ts` 中添加删除方法: ```js -import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==> +import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==> import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==> @@ -1323,11 +1322,11 @@ delete(id: string) { `delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时分派动作. 确认弹层看起来如下: -![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) +![booktore-confirmation-popup](./images/booktore-confirmation-popup.png) #### 添加删除按钮 -打开 `app\books\book-list` 文件夹下的 `app\books\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮: +打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮: ```html
@@ -1340,7 +1339,7 @@ delete(id: string) { 最终操作下拉框UI看起来如下: -![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) +![booktore-final-actions-dropdown](./images/booktore-final-actions-dropdown.png) {{end}} diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png index a3197b6457..ffa8dcd7e2 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png and b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png index 9e6cc9e010..d402895c9b 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png and b/docs/zh-Hans/Tutorials/images/bookstore-book-list.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 index 6f19dcc7bf..13829db60f 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png and b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png new file mode 100644 index 0000000000..c935a6f130 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-module-terminal.png differ diff --git a/docs/zh-Hans/Tutorials/images/generated-proxies.png b/docs/zh-Hans/Tutorials/images/generated-proxies.png index 9e466e7d55..1d322c0765 100644 Binary files a/docs/zh-Hans/Tutorials/images/generated-proxies.png and b/docs/zh-Hans/Tutorials/images/generated-proxies.png differ diff --git a/docs/zh-Hans/UI/Angular/Confirmation-Service.md b/docs/zh-Hans/UI/Angular/Confirmation-Service.md index a1a181837c..7f47c7d8a1 100644 --- a/docs/zh-Hans/UI/Angular/Confirmation-Service.md +++ b/docs/zh-Hans/UI/Angular/Confirmation-Service.md @@ -49,7 +49,7 @@ this.confirmation - `message` 和 `title` 参数接收字符串,本地化Key或本地化对象. 参阅[本地化文档](./Localization.md) - `Confirmation.Status` 是一个枚举,具有三个属性; - `Confirmation.Status.confirm` 是一个关闭事件值,当通过确认按钮关闭弹出窗口时触发此事件. - - `Confirmation.Status.reject` 是一个关闭事件值,当通过“取消”按钮关闭弹出窗口时触发此事件. + - `Confirmation.Status.reject` 是一个关闭事件值,当通过取消按钮关闭弹出窗口时触发此事件. - `Confirmation.Status.dismiss` 是一个关闭事件值,当通过按Escape键关闭弹出窗口时触发此事件. 如果你对确认状态不感兴趣,则不必订阅返回的observable: @@ -66,6 +66,7 @@ this.confirmation.error('You are not authorized.', 'Error'); const options: Partial = { hideCancelBtn: false, hideYesBtn: false, + dismissible: false, cancelText: 'Close', yesText: 'Confirm', messageLocalizationParams: ['Demo'], @@ -81,8 +82,9 @@ this.confirmation.warn( - `hideCancelBtn` 选项为 `true` 时隐藏取消按钮. 默认值为 `false`. - `hideYesBtn` 选项为 `true` 时隐藏确认按钮. 默认值为 `false`. -- `cancelText` 是取消按钮的文本,可以传递本地化键或本地化对象. 默认值是 `AbpUi::Cancel`. -- `yesText` 是确定按钮的文本,可以传递本地化键或本地化对象. 默认值是 `AbpUi::Yes`. +- `dismissible`选项允许通过按Escape键或单击背景来取消确认弹出窗口. 默认值为 `true`. +- `cancelText` 是取消按钮的文本,可以传递本地化键或本地化对象. 默认值为 `AbpUi::Cancel`. +- `yesText` 是确定按钮的文本,可以传递本地化键或本地化对象. 默认值为 `AbpUi::Yes`. - `messageLocalizationParams`是用于消息本地化的插值参数. - `titleLocalizationParams` 是标题本地化的插值参数. diff --git a/docs/zh-Hans/UI/Angular/Track-By-Service.md b/docs/zh-Hans/UI/Angular/Track-By-Service.md index 7b664141ff..53c9dcbf1c 100644 --- a/docs/zh-Hans/UI/Angular/Track-By-Service.md +++ b/docs/zh-Hans/UI/Angular/Track-By-Service.md @@ -96,3 +96,7 @@ class DemoComponent { trackByTenantAccountId = trackByDeep('tenant', 'account', 'id'); } ``` + +## 下一步是什么? + +- [ListService](./List-Service.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md index 7a9b3de126..945da6fa67 100644 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } } diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index c70284637d..a3becc073b 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -30,8 +30,16 @@ "text": "应用开发", "items": [ { - "text": "使用 ASP.NET Core MVC", - "path": "Tutorials/AspNetCore-Mvc/Part-I.md" + "text": "第一章: 创建新解决方案和列表页", + "path": "Tutorials/Part-1.md" + }, + { + "text": "第一章: 增删改查操作", + "path": "Tutorials/Part-2.md" + }, + { + "text": "第三章: 集成测试", + "path": "Tutorials/Part-3.md" } ] } @@ -173,6 +181,10 @@ "path": "Object-To-Object-Mapping.md" }, + { + "text": "文本模板", + "path": "Text-Templating.md" + }, { "text": "JSON序列化" }, @@ -487,6 +499,10 @@ { "text": "示例", "items": [ + { + "text": "所有示例", + "path": "Samples/Index.md" + }, { "text": "微服务示例", "path": "Samples/Microservice-Demo.md" diff --git a/docs/zh-Hans/images/signal-js-file.png b/docs/zh-Hans/images/signal-js-file.png new file mode 100644 index 0000000000..19fc6cefe3 Binary files /dev/null and b/docs/zh-Hans/images/signal-js-file.png differ diff --git a/docs/zh-Hans/images/signalr-demo-chat.png b/docs/zh-Hans/images/signalr-demo-chat.png new file mode 100644 index 0000000000..120d7eda6e Binary files /dev/null and b/docs/zh-Hans/images/signalr-demo-chat.png differ diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 5d6f3e99a2..8e8dac2ce5 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -283,7 +283,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TextTemplating", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TextTemplating.Tests", "test\Volo.Abp.TextTemplating.Tests\Volo.Abp.TextTemplating.Tests.csproj", "{251C7FD3-D313-4BCE-8068-352EC7EEA275}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Validation.Abstractions", "src\Volo.Abp.Validation.Abstractions\Volo.Abp.Validation.Abstractions.csproj", "{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Validation.Abstractions", "src\Volo.Abp.Validation.Abstractions\Volo.Abp.Validation.Abstractions.csproj", "{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.SignalR", "src\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj", "{B64FCE08-E9D2-4984-BF12-FE199F257416}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.SignalR.Tests", "test\Volo.Abp.AspNetCore.SignalR.Tests\Volo.Abp.AspNetCore.SignalR.Tests.csproj", "{8B758716-DCC9-4223-8421-5588D1597487}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests", "test\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Tests.csproj", "{79323211-E658-493E-9863-035AA4C3F913}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -847,6 +853,18 @@ Global {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Release|Any CPU.Build.0 = Release|Any CPU + {B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.Build.0 = Release|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Release|Any CPU.Build.0 = Release|Any CPU + {79323211-E658-493E-9863-035AA4C3F913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79323211-E658-493E-9863-035AA4C3F913}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79323211-E658-493E-9863-035AA4C3F913}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79323211-E658-493E-9863-035AA4C3F913}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -991,6 +1009,9 @@ Global {9E53F91F-EACD-4191-A487-E727741F1311} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {251C7FD3-D313-4BCE-8068-352EC7EEA275} = {447C8A77-E5F0-4538-8687-7383196D04EA} {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {B64FCE08-E9D2-4984-BF12-FE199F257416} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {8B758716-DCC9-4223-8421-5588D1597487} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {79323211-E658-493E-9863-035AA4C3F913} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs index 702af56dba..2197d7b08e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs @@ -12,5 +12,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations public Guid? TenantId { get; set; } public string UserName { get; set; } + + public string Email { get; set; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs index 83093e0370..d15870e7c1 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs @@ -9,6 +9,5 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending public Dictionary Properties { get; set; } public Dictionary Configuration { get; set; } - } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumDto.cs new file mode 100644 index 0000000000..4f2dc526eb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending +{ + [Serializable] + public class ExtensionEnumDto + { + public List Fields { get; set; } + + public string LocalizationResource { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumFieldDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumFieldDto.cs new file mode 100644 index 0000000000..ddc7c349d4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionEnumFieldDto.cs @@ -0,0 +1,12 @@ +using System; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending +{ + [Serializable] + public class ExtensionEnumFieldDto + { + public string Name { get; set; } + + public object Value { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs index c1c19350c2..12941a3b89 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs @@ -1,36 +1,13 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Volo.Abp.Reflection; namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending { [Serializable] public class ExtensionPropertyAttributeDto { - public string Type { get; set; } public string TypeSimple { get; set; } - public Dictionary Configuration { get; set; } - public static ExtensionPropertyAttributeDto Create(Attribute attribute) - { - var attributeType = attribute.GetType(); - var dto = new ExtensionPropertyAttributeDto - { - Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(attributeType), - TypeSimple = TypeHelper.GetSimplifiedName(attributeType), - Configuration = new Dictionary() - }; - - if (attribute is StringLengthAttribute stringLengthAttribute) - { - dto.Configuration["MaximumLength"] = stringLengthAttribute.MaximumLength; - dto.Configuration["MinimumLength"] = stringLengthAttribute.MinimumLength; - } - - //TODO: Others! - - return dto; - } + public Dictionary Config { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs index 74214914b9..b1ba6f7ea9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs @@ -21,5 +21,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending public List Attributes { get; set; } public Dictionary Configuration { get; set; } + + public object DefaultValue { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs index fd9665eab1..0338bbd4dc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs @@ -7,5 +7,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending public class ObjectExtensionsDto { public Dictionary Modules { get; set; } + + public Dictionary Enums { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs index 9f047c5b3c..700a5f4ee9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs @@ -21,38 +21,30 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers public string GetLocalizedText(string text, ModelExplorer explorer) { - var resourceType = GetResourceTypeFromModelExplorer(explorer); - var localizer = GetStringLocalizer(resourceType); - - return localizer == null ? text : localizer[text].Value; - } - - public IStringLocalizer GetLocalizer(ModelExplorer explorer) - { - var resourceType = GetResourceTypeFromModelExplorer(explorer); - return GetStringLocalizer(resourceType); - } - - public IStringLocalizer GetLocalizer(Assembly assembly) - { - var resourceType = _options.AssemblyResources.GetOrDefault(assembly); - return GetStringLocalizer(resourceType); + var localizer = GetLocalizerOrNull(explorer); + return localizer == null + ? text + : localizer[text].Value; } - public IStringLocalizer GetLocalizer(Type resourceType) + public IStringLocalizer GetLocalizerOrNull(ModelExplorer explorer) { - return GetStringLocalizer(resourceType); + return GetLocalizerOrNull(explorer.Container.ModelType.Assembly); } - private IStringLocalizer GetStringLocalizer(Type resourceType) + public IStringLocalizer GetLocalizerOrNull(Assembly assembly) { - return resourceType == null ? null : _stringLocalizerFactory.Create(resourceType); + var resourceType = GetResourceType(assembly); + return resourceType == null + ? _stringLocalizerFactory.CreateDefaultOrNull() + : _stringLocalizerFactory.Create(resourceType); } - private Type GetResourceTypeFromModelExplorer(ModelExplorer explorer) + private Type GetResourceType(Assembly assembly) { - var assembly = explorer.Container.ModelType.Assembly; - return _options.AssemblyResources.GetOrDefault(assembly); + return _options + .AssemblyResources + .GetOrDefault(assembly); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs index d925fd11e8..761b72ec99 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperExtensions.cs @@ -2,20 +2,31 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.TagHelpers; using System.Text.Encodings.Web; -using Volo.Abp.Threading; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions { public static class TagHelperExtensions { - public static async Task ProcessAndGetOutputAsync(this TagHelper tagHelper, TagHelperAttributeList attributeList, TagHelperContext context, string tagName = "div", TagMode tagMode = TagMode.SelfClosing) + public static async Task ProcessAndGetOutputAsync( + this TagHelper tagHelper, + TagHelperAttributeList attributeList, + TagHelperContext context, + string tagName = "div", + TagMode tagMode = TagMode.SelfClosing) { - var innerOutput = new TagHelperOutput(tagName, attributeList, (useCachedResult, encoder) => Task.Run(() => new DefaultTagHelperContent())) + var innerOutput = new TagHelperOutput( + tagName, + attributeList, + (useCachedResult, encoder) => Task.Run(() => new DefaultTagHelperContent())) { TagMode = tagMode }; - - var innerContext = new TagHelperContext(attributeList, context.Items, Guid.NewGuid().ToString()); + + var innerContext = new TagHelperContext( + attributeList, + context.Items, + Guid.NewGuid().ToString() + ); tagHelper.Init(context); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs index f264d21cd5..16a700f4bc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs @@ -33,6 +33,13 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form [ViewContext] public ViewContext ViewContext { get; set; } + [HtmlAttributeName("asp-format")] + public string Format { get; set; } + + public string Name { get; set; } + + public string Value { get; set; } + public AbpInputTagHelper(AbpInputTagHelperService tagHelperService) : base(tagHelperService) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index a2bb92a14d..4ddf44d2d6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -91,7 +91,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form inputHtml + label : label + inputHtml; - return innerContent + infoHtml + validation; + return innerContent + infoHtml + validation; } protected virtual string SurroundInnerHtmlAndGet(TagHelperContext context, TagHelperOutput output, string innerHtml, bool isCheckbox) @@ -103,30 +103,56 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual TagHelper GetInputTagHelper(TagHelperContext context, TagHelperOutput output) { - var textAreaAttribute = TagHelper.AspFor.ModelExplorer.GetAttribute