diff --git a/docs/en/UI/Angular/List-Service.md b/docs/en/UI/Angular/List-Service.md index 900606d725..f65f0db4cd 100644 --- a/docs/en/UI/Angular/List-Service.md +++ b/docs/en/UI/Angular/List-Service.md @@ -120,7 +120,7 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide ``` -> We donot recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page. +> We do not recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page. ## How to Refresh Table on Create/Update/Delete diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md index 5b4b766a27..3992c7d8a1 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -11,92 +11,95 @@ Basic usage: ````xml ```` + Model: + ````csharp public class DynamicFormsModel : PageModel - { - [BindProperty] - public DetailedModel MyDetailedModel { get; set; } +{ + [BindProperty] + public DetailedModel MyDetailedModel { get; set; } - public List CountryList { get; set; } = new List - { - new SelectListItem { Value = "CA", Text = "Canada"}, - new SelectListItem { Value = "US", Text = "USA"}, - new SelectListItem { Value = "UK", Text = "United Kingdom"}, - new SelectListItem { Value = "RU", Text = "Russia"} - }; + public List CountryList { get; set; } = new List + { + new SelectListItem { Value = "CA", Text = "Canada"}, + new SelectListItem { Value = "US", Text = "USA"}, + new SelectListItem { Value = "UK", Text = "United Kingdom"}, + new SelectListItem { Value = "RU", Text = "Russia"} + }; - public void OnGet() - { - MyDetailedModel = new DetailedModel - { - Name = "", - Description = "Lorem ipsum dolor sit amet.", - IsActive = true, - Age = 65, - Day = DateTime.Now, - MyCarType = CarType.Coupe, - YourCarType = CarType.Sedan, - Country = "RU", - NeighborCountries = new List() { "UK", "CA" } - }; - } + public void OnGet() + { + MyDetailedModel = new DetailedModel + { + Name = "", + Description = "Lorem ipsum dolor sit amet.", + IsActive = true, + Age = 65, + Day = DateTime.Now, + MyCarType = CarType.Coupe, + YourCarType = CarType.Sedan, + Country = "RU", + NeighborCountries = new List() { "UK", "CA" } + }; + } - public class DetailedModel - { - [Required] - [Placeholder("Enter your name...")] - [Display(Name = "Name")] - public string Name { get; set; } - - [TextArea(Rows = 4)] - [Display(Name = "Description")] - [InputInfoText("Describe Yourself")] - public string Description { get; set; } - - [Required] - [DataType(DataType.Password)] - [Display(Name = "Password")] - public string Password { get; set; } - - [Display(Name = "Is Active")] - public bool IsActive { get; set; } - - [Required] - [Display(Name = "Age")] - public int Age { get; set; } - - [Required] - [Display(Name = "My Car Type")] - public CarType MyCarType { get; set; } - - [Required] - [AbpRadioButton(Inline = true)] - [Display(Name = "Your Car Type")] - public CarType YourCarType { get; set; } - - [DataType(DataType.Date)] - [Display(Name = "Day")] - public DateTime Day { get; set; } - - [SelectItems(nameof(CountryList))] - [Display(Name = "Country")] - public string Country { get; set; } - - [SelectItems(nameof(CountryList))] - [Display(Name = "Neighbor Countries")] - public List NeighborCountries { get; set; } - } + public class DetailedModel + { + [Required] + [Placeholder("Enter your name...")] + [Display(Name = "Name")] + public string Name { get; set; } + + [TextArea(Rows = 4)] + [Display(Name = "Description")] + [InputInfoText("Describe Yourself")] + public string Description { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Is Active")] + public bool IsActive { get; set; } + + [Required] + [Display(Name = "Age")] + public int Age { get; set; } + + [Required] + [Display(Name = "My Car Type")] + public CarType MyCarType { get; set; } + + [Required] + [AbpRadioButton(Inline = true)] + [Display(Name = "Your Car Type")] + public CarType YourCarType { get; set; } + + [DataType(DataType.Date)] + [Display(Name = "Day")] + public DateTime Day { get; set; } + + [SelectItems(nameof(CountryList))] + [Display(Name = "Country")] + public string Country { get; set; } + + [SelectItems(nameof(CountryList))] + [Display(Name = "Neighbor Countries")] + public List NeighborCountries { get; set; } + } - public enum CarType - { - Sedan, - Hatchback, - StationWagon, - Coupe - } + public enum CarType + { + Sedan, + Hatchback, + StationWagon, + Coupe } +} ```` + ## Demo See the [dynamic forms demo page](https://bootstrap-taghelpers.abp.io/Components/DynamicForms) to see it in action. @@ -125,7 +128,7 @@ Default value is `True`. ## Form Content Placement -By default, `abp-dynamicform` clears the inner html and places the inputs into itself. If you want to add additional content to dynamic form or place the inputs to some specific area, you can use ` ` tag. This tag will be replaced by form content and rest of the inner html of `abp-dynamic-form` tag will be unchanged. +By default, `abp-dynamic-form` clears the inner html and places the inputs into itself. If you want to add additional content to dynamic form or place the inputs to some specific area, you can use ` ` tag. This tag will be replaced by form content and rest of the inner html of `abp-dynamic-form` tag will be unchanged. Usage: @@ -152,23 +155,23 @@ Default `DisplayOrder` attribute number is 10000 for every property. See example below: ````csharp - public class OrderExampleModel - { - [DisplayOrder(10004)] - public string Name{ get; set; } - - [DisplayOrder(10005)] - public string Surname{ get; set; } - - //Default 10000 - public string EmailAddress { get; set; } - - [DisplayOrder(10003)] - public string PhoneNumber { get; set; } - - [DisplayOrder(9999)] - public string City { get; set; } - } +public class OrderExampleModel +{ + [DisplayOrder(10004)] + public string Name{ get; set; } + + [DisplayOrder(10005)] + public string Surname{ get; set; } + + //Default 10000 + public string EmailAddress { get; set; } + + [DisplayOrder(10003)] + public string PhoneNumber { get; set; } + + [DisplayOrder(9999)] + public string City { get; set; } +} ```` In this example, input fields will be displayed with this order: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`. @@ -198,55 +201,57 @@ If you have read the [Form elements document](Form-elements.md), you noticed tha ````xml ```` + Model: + ````csharp public class DynamicFormsModel : PageModel - { - [BindProperty] - public DetailedModel MyDetailedModel { get; set; } +{ + [BindProperty] + public DetailedModel MyDetailedModel { get; set; } - public List CountryList { get; set; } = new List - { - new SelectListItem { Value = "CA", Text = "Canada"}, - new SelectListItem { Value = "US", Text = "USA"}, - new SelectListItem { Value = "UK", Text = "United Kingdom"}, - new SelectListItem { Value = "RU", Text = "Russia"} - }; + public List CountryList { get; set; } = new List + { + new SelectListItem { Value = "CA", Text = "Canada"}, + new SelectListItem { Value = "US", Text = "USA"}, + new SelectListItem { Value = "UK", Text = "United Kingdom"}, + new SelectListItem { Value = "RU", Text = "Russia"} + }; - public void OnGet() - { - MyDetailedModel = new DetailedModel - { - ComboCarType = CarType.Coupe, - RadioCarType = CarType.Sedan, - ComboCountry = "RU", - RadioCountry = "UK" - }; - } + public void OnGet() + { + MyDetailedModel = new DetailedModel + { + ComboCarType = CarType.Coupe, + RadioCarType = CarType.Sedan, + ComboCountry = "RU", + RadioCountry = "UK" + }; + } - public class DetailedModel - { - public CarType ComboCarType { get; set; } - - [AbpRadioButton(Inline = true)] - public CarType RadioCarType { get; set; } - - [SelectItems(nameof(CountryList))] - public string ComboCountry { get; set; } - - [AbpRadioButton()] - [SelectItems(nameof(CountryList))] - public string RadioCountry { get; set; } - } + public class DetailedModel + { + public CarType ComboCarType { get; set; } + + [AbpRadioButton(Inline = true)] + public CarType RadioCarType { get; set; } + + [SelectItems(nameof(CountryList))] + public string ComboCountry { get; set; } + + [AbpRadioButton()] + [SelectItems(nameof(CountryList))] + public string RadioCountry { get; set; } + } - public enum CarType - { - Sedan, - Hatchback, - StationWagon, - Coupe - } + public enum CarType + { + Sedan, + Hatchback, + StationWagon, + Coupe } +} ```` As you see in example above: @@ -265,13 +270,6 @@ By default, it will try to find "DisplayName:{PropertyName}" or "{PropertyName}" You can set it yourself by using `[Display()]` attribute of Asp.Net Core. You can use a localization key in this attribute. See example below: ````csharp - [Display(Name = "Name")] - public string Name { get; set; } -```` - - - - - - - +[Display(Name = "Name")] +public string Name { get; set; } +```` \ No newline at end of file diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index 788c4a67ed..e733b6e2e5 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -169,7 +169,8 @@ abp update [options] * `--nuget`: 仅更新的NuGet包 * `--solution-path` 或 `-sp`: 指定解决方案路径/目录. 默认使用当前目录 * `--solution-name` 或 `-sn`: 指定解决方案名称. 默认在目录中搜索`*.sln`文件. - +*`--check-all`: 分别检查每个包的新版本. 默认是 `false`. + ### 切换到每晚构建(预览)包 想要切换到ABP框架的最新预览版可以使用此命令. diff --git a/docs/zh-Hans/Entity-Framework-Core-MySQL.md b/docs/zh-Hans/Entity-Framework-Core-MySQL.md index 48297ea68d..75505b883d 100644 --- a/docs/zh-Hans/Entity-Framework-Core-MySQL.md +++ b/docs/zh-Hans/Entity-Framework-Core-MySQL.md @@ -8,14 +8,14 @@ ## 替换模块依赖项 -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreMySQLModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.MySQL;`). +在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreMySQLModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.MySQL;`). ## UseMySQL() 查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. +* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. > 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. diff --git a/docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md b/docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md new file mode 100644 index 0000000000..150ee51f11 --- /dev/null +++ b/docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md @@ -0,0 +1,60 @@ +# 切换到 EF Core Oracle Devart 提供程序 + +本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 + +> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,有关其他选项,请参见[文档](Entity-Framework-Core-Oracle.md). + +## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 + +解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的[Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. + +## 替换模块依赖项 + +在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`). + +## UseOracle() + +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件: + +* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. +* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. + +找到 *YourProjectName*MigrationsDbContextFactory.cs 的 `CreateDbContext()` 方法,将以下代码块 + +```csharp +var builder = new DbContextOptionsBuilder() + .UseSqlServer(configuration.GetConnectionString("Default")); +``` + +替换为: + +```csharp +var builder = (DbContextOptionsBuilder) + new DbContextOptionsBuilder().UseOracle + ( + configuration.GetConnectionString("Default") + ); +``` + +> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. + +## 更改连接字符串 + +Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关Oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/). + +通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. + +## 重新生成迁移 + +启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. + +* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. +* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). + +这将创建一个配置所有数据库对象(表)的数据库迁移. + +运行 `.DbMigrator` 项目创建数据库和初始种子数据. + +## 运行应用程序 + +它已准备就绪, 只需要运行该应用程序与享受编码. diff --git a/docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md b/docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md new file mode 100644 index 0000000000..e5932be3db --- /dev/null +++ b/docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md @@ -0,0 +1,67 @@ +# 切换到EF Core Oracle提供程序 + +本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 + +> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库 + +## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 + +解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. + +## 替换模块依赖项 + +在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`). + +## UseOracle() + +Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files: + +* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. +* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. + +In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block + +查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件: + +* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. +* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. + +使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法: + +```csharp +var builder = new DbContextOptionsBuilder() + .UseSqlServer(configuration.GetConnectionString("Default")); +``` + +与这个 + +```csharp +var builder = (DbContextOptionsBuilder) + new DbContextOptionsBuilder().UseOracle + ( + configuration.GetConnectionString("Default") + ); +``` + +> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. + +## 更改连接字符串 + +Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/). + +通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. + +## 重新生成迁移 + +启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. + +* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. +* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). + +这将创建一个配置所有数据库对象(表)的数据库迁移. + +运行 `.DbMigrator` 项目创建数据库和初始种子数据. + +## 运行应用程序 + +它已准备就绪, 只需要运行该应用程序与享受编码. \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core-Oracle.md b/docs/zh-Hans/Entity-Framework-Core-Oracle.md index ee510e5edc..cedd4345de 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Oracle.md +++ b/docs/zh-Hans/Entity-Framework-Core-Oracle.md @@ -1,67 +1,10 @@ -# 切换到EF Core Oracle提供程序 +# 切换到EF Core Oracle 提供程序 本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序 -> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库 +ABP框架提供了两种不同的Oracle包集成. 你可以选择以下其中一个: -## 替换Volo.Abp.EntityFrameworkCore.SqlServer包 +* **[Volo.Abp.EntityFrameworkCore.Oracle](Entity-Framework-Core-Oracle-Official.md)** 使用官方 & 免费的oracle驱动 ( **当前处于 beta**). +* **[Volo.Abp.EntityFrameworkCore.Oracle.Devart](Entity-Framework-Core-Oracle-Devart.md)** 使用[Devart](https://www.devart.com/)公司提供的商业(付费)驱动. -解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包. - -## 替换模块依赖项 - -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`). - -## UseOracle() - -Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files: - -* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. -* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. - -In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block - -查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件: - -* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. - -使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法: - -```csharp -var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("Default")); -``` - -与这个 - -```csharp -var builder = (DbContextOptionsBuilder) - new DbContextOptionsBuilder().UseOracle - ( - configuration.GetConnectionString("Default") - ); -``` - -> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. - -## 更改连接字符串 - -Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/). - -通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. - -## 重新生成迁移 - -启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败. - -* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案. -* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目). - -这将创建一个配置所有数据库对象(表)的数据库迁移. - -运行 `.DbMigrator` 项目创建数据库和初始种子数据. - -## 运行应用程序 - -它已准备就绪, 只需要运行该应用程序与享受编码. \ No newline at end of file +> 你可以选择一个你想要的包,如果你不知道它们之间的区别,请在网站上进行搜索. ABP框架仅提供集成,不提供第三库类库的支持. \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md index 350cff65b0..6e481bb230 100644 --- a/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md +++ b/docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md @@ -8,14 +8,14 @@ ## 替换模块依赖项 -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). +在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.PostgreSql;`). ## UseNpgsql() 查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseNpgsql()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. +* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. > 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. diff --git a/docs/zh-Hans/Entity-Framework-Core-SQLite.md b/docs/zh-Hans/Entity-Framework-Core-SQLite.md index d75f8ba5b7..646e3082d5 100644 --- a/docs/zh-Hans/Entity-Framework-Core-SQLite.md +++ b/docs/zh-Hans/Entity-Framework-Core-SQLite.md @@ -8,14 +8,14 @@ ## 替换模块依赖项 -在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreSqliteModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Sqlite;`). +在 `.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreSqliteModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;` 为 `using Volo.Abp.EntityFrameworkCore.Sqlite;`). ## UseSqlite() 查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件: * `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs. -* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. +* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs. > 根据你的解决方案的结构,你可能发现更多需要改变代码的文件. diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md index e2472bdcee..9a71643a51 100644 --- a/docs/zh-Hans/Tutorials/Part-1.md +++ b/docs/zh-Hans/Tutorials/Part-1.md @@ -737,8 +737,8 @@ $(function () { 是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具: - [Angular CLI](https://angular.io/cli) 用于创建模块,组件和服务. -- [NGXS](https://ngxs.gitbook.io/ngxs/) 用于管理状态库. - [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库. +- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库. - [Visual Studio Code](https://code.visualstudio.com/) 用做代码编辑器 (你可以选择自己喜欢的编辑器). #### 安装 NPM 包 @@ -761,26 +761,56 @@ yarn ng generate module book --routing true #### 路由 -打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的 `import` 和路由: +打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的路由: ```js -import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== +const routes: Routes = [ +// ... +// added a new route to the routes array + { + path: 'books', + loadChildren: () => import('./book/book.module').then(m => m.BookModule) + } +] +``` -//...added book path with the below to the routes array -{ - path: 'book', - component: ApplicationLayoutComponent, - loadChildren: () => import('./book/book.module').then(m => m.BookModule), - data: { - routes: { - name: '::Menu:Book', - iconClass: 'fas fa-book' - } as ABP.Route - }, +* 我们添加了一个懒加载路由. 参阅 [嬾加載功能模块](https://angular.io/guide/lazy-loading-ngmodules#lazy-loading-feature-modules). + +打开位于 `src\app` 目录下的 `route.provider.ts` 文件,用以下内容替换它: + +```js +import { RoutesService, eLayoutType } from '@abp/ng.core'; +import { APP_INITIALIZER } from '@angular/core'; + +export const APP_ROUTE_PROVIDER = [ + { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, +]; + +function configureRoutes(routes: RoutesService) { + return () => { + routes.add([ + //... + // added below element + { + path: '/books', + name: '::Menu:Books', + iconClass: 'fas fa-book', + order: 101, + layout: eLayoutType.application, + }, + ]); + }; } ``` -* `ApplicationLayoutComponent` 配置将应用程序布局设置为新页面, 我们添加了 `data` 对象. `name` 是菜单项的名称,`iconClass` 是菜单项的图标. +* 我们添加了一个新的路由元素在菜单上显示为 "Books" 的导航元素. + * `path` 路由的URL. + * `name` 菜单项的名称,可以使用本地化Key. + * `iconClass` 菜单项的图标. + * `order` 菜单项的排序.我们定义了101,它显示在 "Administration" 项的后面. + * `layout` BooksModule路由的布局. 可以定义 `eLayoutType.application`, `eLayoutType.account` 或 `eLayoutType.empty`. + +更多信息请参阅[RoutesService 文档](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice). #### Book 列表组件 @@ -848,35 +878,6 @@ yarn start ![Initial book list page](./images/bookstore-initial-book-list-page.png) -#### 创建 BookState - -运行以下命令创建名为 `BookState` 的新state: - -```bash -npx @ngxs/cli --name book --directory src/app/book -``` - -* 此命令在 `src/app/book/state` 文件夹下创建了 `book.state.ts` 和 `book.actions.ts` 文件. 参阅 [NGXS CLI文档](https://www.ngxs.io/plugins/cli)了解更多. - -将 `BookState` 导入到 `src/app` 文件夹中的 `app.module.ts` 中. 然后添加 `BookState` 到 `NgxsModule` 的 `forRoot` 静态方法,作为该方法的第一个参数的数组元素. - -```js -// ... -import { BookState } from './book/state/book.state'; //<== imported BookState ==> - -@NgModule({ - imports: [ - // other imports - - NgxsModule.forRoot([BookState]), //<== added BookState ==> - - //other imports - ], - // ... -}) -export class AppModule {} -``` - #### 生成代理 ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md). @@ -893,109 +894,41 @@ abp generate-proxy --module app ![Generated files](./images/generated-proxies.png) -#### GetBook 动作 - -动作可以被认为是一个命令,它应该触发某些事情发生,或者是已经发生的事情的结果事件.[See NGXS Actions文档](https://www.ngxs.io/concepts/actions). - -打开 `app/book/state` 目录下的 `book.actions.ts` 文件用以下内容替换它: - -```js -export class GetBook { - static readonly type = '[Book] Get'; -} -``` - -#### 实现 BookState - -打开 `app/book/state` 目录下的 `book.state.ts` 文件用以下内容替换它: - -```js -import { PagedResultDto } from '@abp/ng.core'; -import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks } from './book.actions'; -import { BookService } from '../services'; -import { tap } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { BookDto } from '../models'; - -export class BookStateModel { - public book: PagedResultDto; -} - -@State({ - name: 'BookState', - defaults: { book: {} } as BookStateModel, -}) -@Injectable() -export class BookState { - @Selector() - static getBooks(state: BookStateModel) { - return state.book.items || []; - } - - constructor(private bookService: BookService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.bookService.getListByInput().pipe( - tap((booksResponse) => { - ctx.patchState({ - book: booksResponse, - }); - }) - ); - } -} -``` - -* 我们添加了book属性到BookStateModel模态框. -* 我们添加了 `GetBook` 动作. 它通过 ABP CLI生成的 `BookService` 检索图书数据. -* `NGXS` 需要在不订阅get函数的情况下返回被观察对象. - #### BookListComponent 打开 `app\book\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它: ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks } from '../state/book.actions'; -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; @Component({ selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], + providers: [ListService], }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; - loading = false; - - constructor(private store: Store) {} + constructor(public readonly list: ListService, private bookService: BookService) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } } ``` -* 我们添加了 `get` 函数获取book更新store. -* 有关 `NGXS` 特性的更多信息请参见NGXS文档中的[Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions)和[Select](https://ngxs.gitbook.io/ngxs/concepts/select). +* 我们注入了生成的 `BookService`. +* 我们实现了 [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service),它是一个公用服务,提供了简单的分页,排序和搜索. 打开 `app\book\book-list` 目录下的 `book-list.component.html` 用以下内容替换它: @@ -1005,38 +938,31 @@ export class BookListComponent implements OnInit {
- {%{{{ "::Menu:Book" | abpLocalization }}}%} + {%{{{ '::Menu:Books' | abpLocalization }}}%}
- - - - - {%{{{ "::Name" | abpLocalization }}}%} - {%{{{ "::Type" | abpLocalization }}}%} - {%{{{ "::PublishDate" | abpLocalization }}}%} - {%{{{ "::Price" | abpLocalization }}}%} - - - - - {%{{{ data.name }}}%} - {%{{{ bookType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - - + + + + + {%{{{ booksType[row.type] }}}%} + + + + + {%{{{ row.publishDate | date }}}%} + + + + + {%{{{ row.price | currency }}}%} + + +
``` diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md index 74a52802b0..5ddc8c2a66 100644 --- a/docs/zh-Hans/Tutorials/Part-2.md +++ b/docs/zh-Hans/Tutorials/Part-2.md @@ -454,78 +454,48 @@ $(function () { 下面的章节中,你将学习到如何创建一个新的模态对话框来新增Book实体. -#### 状态定义 +#### 添加 modal 到 BookListComponent -在 `app\book\state` 文件夹下打开 `book.action.ts` 文件,使用以下内容替换它: -```js -import { CreateUpdateBookDto } from '../models'; //<== added this line ==> - -export class GetBooks { - static readonly type = '[Book] Get'; -} +Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: -// added CreateUpdateBook class -export class CreateUpdateBook { - static readonly type = '[Book] Create Update Book'; - constructor(public payload: CreateUpdateBookDto) { } -} -``` +```js +import { ListService, PagedResultDto } from '@abp/ng.core'; +import { Component, OnInit } from '@angular/core'; +import { BookDto, BookType } from '../models'; +import { BookService } from '../services'; -* 我们导入了 `CreateUpdateBookDto` 模型并且创建了 `CreateUpdateBook` 动作. +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [ListService], +}) +export class BookListComponent implements OnInit { + book = { items: [], totalCount: 0 } as PagedResultDto; -打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它: + booksType = BookType; -```js -import { PagedResultDto } from '@abp/ng.core'; -import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==> -import { BookService } from '../services'; -import { tap } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { BookDto } from '../models'; + isModalOpen = false; // <== added this line ==> -export class BookStateModel { - public book: PagedResultDto; -} + constructor(public readonly list: ListService, private bookService: BookService) {} -@State({ - name: 'BookState', - defaults: { book: {} } as BookStateModel, -}) -@Injectable() -export class BookState { - @Selector() - static getBooks(state: BookStateModel) { - return state.book.items || []; - } + ngOnInit() { + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - constructor(private bookService: BookService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.bookService.getListByInput().pipe( - tap((bookResponse) => { - ctx.patchState({ - book: bookResponse, - }); - }) - ); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } - // added CreateUpdateBook action listener - @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { - return this.bookService.createByInput(action.payload); + // added createBook method + createBook() { + this.isModalOpen = true; } } ``` -* 我们导入了 `CreateUpdateBook` 动作并且定义了 `save` 方法监听 `CreateUpdateBook` 动作去创建图书. - -当 `SaveBook` 动作被分派时,save方法被执行. 它调用 `BookService` 的 `createByInput` 方法. - -#### 添加模态到 BookListComponent +* 我们定义了一个名为 `isModalOpen` 的变量和 `createBook` 方法. 打开 `app\book\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它: @@ -534,19 +504,12 @@ export class BookState {
-
- {%{{{ '::Menu:Books' | abpLocalization }}}%} -
+
{%{{{ '::Menu:Books' | abpLocalization }}}%}
- +
- @@ -555,47 +518,40 @@ export class BookState {
- - - - - {%{{{ "::Name" | abpLocalization }}}%} - {%{{{ "::Type" | abpLocalization }}}%} - {%{{{ "::PublishDate" | abpLocalization }}}%} - {%{{{ "::Price" | abpLocalization }}}%} - - - - - {%{{{ data.name }}}%} - {%{{{ bookType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - - + + + + + {%{{{ booksType[row.type] }}}%} + + + + + {%{{{ row.publishDate | date }}}%} + + + + + {%{{{ row.price | currency }}}%} + + +
- -

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

-
+ +

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

+
- + - - - + + +
``` @@ -603,55 +559,6 @@ export class BookState { * `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处. * 我们添加了 `New book` 按钮到 `AbpContentToolbar`. -打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: - -```js -import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../models'; -import { GetBooks } from '../state/book.actions'; -import { BookState } from '../state/book.state'; - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], -}) -export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; - - booksType = BookType; - - loading = false; - - isModalOpen = false; // <== added this line ==> - - constructor(private store: Store) {} - - ngOnInit() { - this.get(); - } - - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); - } - - // added createBook method - createBook() { - this.isModalOpen = true; - } -} -``` - -* 我们添加了 `isModalOpen = false` 和 `createBook` 方法. - 你可以打开浏览器,点击**New book**按钮看到模态框. ![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) @@ -660,51 +567,46 @@ export class BookListComponent implements OnInit { [响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入. -打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: +打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks } from '../state/book.actions'; -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> @Component({ selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], + providers: [ListService], }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; - loading = false; - isModalOpen = false; form: FormGroup; // <== added this line ==> - constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==> + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder // <== injected FormBuilder ==> + ) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } createBook() { - this.buildForm(); //<== added this line ==> + this.buildForm(); // <== added this line ==> this.isModalOpen = true; } @@ -729,7 +631,7 @@ export class BookListComponent implements OnInit { #### 创建表单的DOM元素 -打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 ` `: +打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 ` `: ```html @@ -798,16 +700,13 @@ export class BooksModule { } * 我们导入了 `NgbDatepickerModule` 来使用日期选择器. -打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: +打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks } from '../state/book.actions'; -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==> @@ -815,37 +714,34 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==> + providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added a provide ==> }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; - //added bookTypeArr array + // <== added bookTypeArr array ==> bookTypeArr = Object.keys(BookType).filter( (bookType) => typeof this.booksType[bookType] === 'number' ); - loading = false; - isModalOpen = false; form: FormGroup; - constructor(private store: Store, private fb: FormBuilder) {} + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder + ) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } createBook() { @@ -882,16 +778,13 @@ export class BookListComponent implements OnInit { #### 保存图书 -打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: +打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==> -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; @@ -899,11 +792,10 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], + providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - books$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; booksType = BookType; @@ -911,24 +803,22 @@ export class BookListComponent implements OnInit { (bookType) => typeof this.booksType[bookType] === 'number' ); - loading = false; - isModalOpen = false; form: FormGroup; - constructor(private store: Store, private fb: FormBuilder) {} + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder + ) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } createBook() { @@ -951,19 +841,18 @@ export class BookListComponent implements OnInit { return; } - this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { + this.bookService.createByInput(this.form.value).subscribe(() => { this.isModalOpen = false; this.form.reset(); - this.get(); + this.list.get(); }); } } ``` -* 我们导入了 `CreateUpdateBook`. * 我们添加了 `save` 方法. -打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list`文件, 添加 `abp-button` 保存图书. +打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件, 找到 `` 元素,使用下面元素替换它: ```html @@ -994,90 +883,49 @@ export class BookListComponent implements OnInit { ### 更新图书 -#### CreateUpdateBook 动作 - -打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件,使用以下内容替换它: - -```js -import { CreateUpdateBookDto } from '../models'; - -export class GetBooks { - static readonly type = '[Book] Get'; -} - -export class CreateUpdateBook { - static readonly type = '[Book] Create Update Book'; - constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==> -} -``` - -* 我们在 `CreateUpdateBook` 动作的构造函数添加了 `id` 参数. - -打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换 `save` 方法: - -```js -@Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - if (action.id) { - return this.bookService.updateByIdAndInput(action.payload, action.id); - } else { - return this.bookService.createByInput(action.payload); - } -} -``` - -#### BookListComponent - -打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,在构造函数注入 `BookService` 服务,并添加 名为 `selectedBook` 的变量. +打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件并且添加名为 `selectedBook` 的变量. ```js +import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; import { BookDto, BookType } from '../models'; -import { GetBooks, CreateUpdateBook } from '../state/book.actions'; -import { BookState } from '../state/book.state'; +import { BookService } from '../services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { BookService } from '../services'; // <== imported BookService ==> @Component({ selector: 'app-book-list', templateUrl: './book-list.component.html', styleUrls: ['./book-list.component.scss'], - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], + providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { - @Select(BookState.getBooks) - book$: Observable; + book = { items: [], totalCount: 0 } as PagedResultDto; - bookType = BookType; + booksType = BookType; bookTypeArr = Object.keys(BookType).filter( - (bookType) => typeof this.bookType[bookType] === 'number' + (bookType) => typeof this.booksType[bookType] === 'number' ); - loading = false; - isModalOpen = false; form: FormGroup; selectedBook = {} as BookDto; // <== declared selectedBook ==> - constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==> + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder + ) {} ngOnInit() { - this.get(); - } + const bookStreamCreator = (query) => this.bookService.getListByInput(query); - get() { - this.loading = true; - this.store - .dispatch(new GetBooks()) - .pipe(finalize(() => (this.loading = false))) - .subscribe(() => {}); + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); } // <== this method is replaced ==> @@ -1109,58 +957,46 @@ export class BookListComponent implements OnInit { }); } + // <== this method is replaced ==> save() { if (this.form.invalid) { return; } - //<== added this.selectedBook.id ==> - this.store - .dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) - .subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.get(); - }); + // <== added request ==> + const request = this.selectedBook.id + ? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id) + : this.bookService.createByInput(this.form.value); + + request.subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.list.get(); + }); } } ``` -* 我们导入了 `BookService`. * 我们声明了类型为 `BookDto` 的 `selectedBook` 变量. -* 我们在构造函数注入了 `BookService`, 它用于检索正在编辑的图书数据. * 我们添加了 `editBook` 方法, 根据给定图书 `Id` 设置 `selectedBook` 对象. * 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单. * 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象. -* 我们在 `CreateUpdateBook` 构造函数添加了 `selectedBook.id`. +* 我们替换了 `save` 方法. #### 添加 "Actions" 下拉框到表格 -打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `
` 标签: +打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `
` 标签: ```html
- - - - - {%{{{ "::Actions" | abpLocalization }}}%} - {%{{{ "::Name" | abpLocalization }}}%} - {%{{{ "::Type" | abpLocalization }}}%} - {%{{{ "::PublishDate" | abpLocalization }}}%} - {%{{{ "::Price" | abpLocalization }}}%} - - - - - + + + +
-
- - {%{{{ data.name }}}%} - {%{{{ bookType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - -
+
+ + + + + {%{{{ booksType[row.type] }}}%} + + + + + {%{{{ row.publishDate | date }}}%} + + + + + {%{{{ row.price | currency }}}%} + + +
``` - 我们添加了 "Actions" 栏的 `th`. +- 我们为 "Actions" 栏添加了 `ngx-datatable-column`. - 我们添加了带有 `ngbDropdownToggle` 的 `button`,在点击按钮时打开操作. - 我们习惯于将[NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples)用于操作的下拉菜单. @@ -1194,7 +1043,7 @@ UI最终看起来像这样: ![Action buttons](./images/bookstore-actions-buttons.png) -打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `` 标签: +打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `` 标签: ```html @@ -1206,80 +1055,9 @@ UI最终看起来像这样: ### 删除图书 -#### DeleteBook 动作 - -打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件添加名为 `DeleteBook` 的动作. - -```js -export class DeleteBook { - static readonly type = '[Book] Delete'; - constructor(public id: string) {} -} -``` - -打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它: - -```js -import { PagedResultDto } from '@abp/ng.core'; -import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==> -import { BookService } from '../services'; -import { tap } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { BookDto } from '../models'; - -export class BookStateModel { - public book: PagedResultDto; -} - -@State({ - name: 'BookState', - defaults: { book: {} } as BookStateModel, -}) -@Injectable() -export class BookState { - @Selector() - static getBooks(state: BookStateModel) { - return state.book.items || []; - } - - constructor(private bookService: BookService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.bookService.getListByInput().pipe( - tap((booksResponse) => { - ctx.patchState({ - book: booksResponse, - }); - }) - ); - } - - @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { - if (action.id) { - return this.bookService.updateByIdAndInput(action.payload, action.id); - } else { - return this.bookService.createByInput(action.payload); - } - } - - // <== added DeleteBook action listener ==> - @Action(DeleteBook) - delete(ctx: StateContext, action: DeleteBook) { - return this.bookService.deleteById(action.id); - } -} -``` - -- 我们导入了 `DeleteBook` . - -- 我们在文件末尾添加了 `DeleteBook` 动作监听器. - #### 删除确认弹层 -打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`. +打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`. 替换构造函数: @@ -1288,50 +1066,46 @@ import { ConfirmationService } from '@abp/ng.theme.shared'; //... constructor( - private store: Store, - private fb: FormBuilder, - private bookService: BookService, - private confirmation: ConfirmationService // <== added this line ==> -) { } + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder, + private confirmation: ConfirmationService // <== added this line ==> +) {} ``` -* 我们导入了 `ConfirmationService`. -* 我们在构造函数注入了 `ConfirmationService` . +* We imported `ConfirmationService`. +* We injected `ConfirmationService` to the constructor. -参阅[确认弹层文档](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)了解更多 +See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service) -在 `book-list.component.ts` 中添加删除方法: +In the `book-list.component.ts` add a delete method: ```js -import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==> - -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==> +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation namespace ==> //... delete(id: string) { - this.confirmation - .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') - .subscribe(status => { - if (status === Confirmation.Status.confirm) { - this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get()); - } - }); + this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => { + if (status === Confirmation.Status.confirm) { + this.bookService.deleteById(id).subscribe(() => this.list.get()); + } + }); } ``` -`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时分派动作. 确认弹层看起来如下: +`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时调用 `BookService` 的 `deleteById` 方法. 确认弹层看起来如下: ![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) #### 添加删除按钮 -打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮: +打开 `app\book\book-list` 文件夹下的 `app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮: ```html
-
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png b/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png index e8243fedc7..f5d8f0870b 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png and b/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png differ 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 ffa8dcd7e2..d921f146fc 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 d402895c9b..ecdb87c737 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-confirmation-popup.png b/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png index a80b180f1c..b17e70638d 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png and b/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png b/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png index 2a02802bb9..f4e969ae22 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png and b/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png differ diff --git a/docs/zh-Hans/UI/Angular/List-Service.md b/docs/zh-Hans/UI/Angular/List-Service.md index 6d5bc6f1ef..39275fdf10 100644 --- a/docs/zh-Hans/UI/Angular/List-Service.md +++ b/docs/zh-Hans/UI/Angular/List-Service.md @@ -53,57 +53,38 @@ class BookComponent { > 注意 `list` 是 `public` 并且 `readonly`. 因为我们将直接在组件的模板中使用 `ListService` 属性. 可以视为反模式,但是实现起来要快得多. 你可以改为使用公共组件属性. -将 `ListService` 属性放入模板中,如下所示: +像这样绑定 `ListService` 到 ngx-datatable: ```html - - - - - - - {%{{{ '::Name' | abpLocalization }}}%} - - - - - - - - {%{{{ data.name }}}%} - - + + ``` ## 与Observables一起使用 你可以将Observables与Angular的[AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe)结合使用: -```ts +```js book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query)); ``` ```html - - + + ``` @@ -111,7 +92,7 @@ class BookComponent { ...or... -```ts +```js @Select(BookState.getBooks) books$: Observable; @@ -126,13 +107,18 @@ class BookComponent { ```html - - + + ``` +> 我们不建议将NGXS存储用于CRUD页面,除非你的应用程序需要在组件之间共享列表信息或稍后在另一页面中使用它. + ## 如何在创建/更新/删除时刷新表 `ListService` 公开了一个 `get` 方法来触发当前查询的请求. 因此基本上每当创建,更新或删除操作解析时,你可以调用 `this.list.get();` 它会调用钩子流创建者. @@ -161,3 +147,26 @@ this.bookService.createByInput(form.value) ``` + +## ABP v3.0的重大更改 + +我们必须修改 `ListService` 使其与 `ngx-datatable` 一起使用. 之前 `page` 属性的最小值为 `1`, 你可以像这样使用它: + +```html + + +``` + +从v3.0开始, 对于`ngx-datatable`, 初始页面的 `page`属性必须设置为 `0`. 因此如果你以前在表上使用过 `ListService` 并打算保留 `abp-table`,则需要进行以下更改: + +```html + + +``` + +**重要提示:** `abp-table` 没有被删除,但是会被弃用,并在将来的版本中移除,请考虑切换到 ngx-datatable. \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Migration-Guide-v3.md b/docs/zh-Hans/UI/Angular/Migration-Guide-v3.md new file mode 100644 index 0000000000..36afffa54f --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Migration-Guide-v3.md @@ -0,0 +1,363 @@ +# Angular UI v2.9 迁移到 v3.0 指南 + +## 在v3.0改变了什么? + +### Angular 10 + +新的ABP Angular UI基于Angular 10和TypeScript 3.9,我们已经放弃了对Angular 8的支持. 不过ABP模块将继续与Angular 9兼容使用. 因此如果你的项目是Angular 9,则无需更新为 Angular10. 更新通常很容易. + +#### 如何迁移? + +在你的根文件夹中打开一个终端,然后运行以下命令: + +```sh +yarn ng update @angular/cli @angular/core --force +``` + +这会做如下修改: + +- 更新你的package.json并安装新的软件包 +- 修改tsconfig.json文件创建一个"Solution Style"配置 +- 重命名 `browserlist` 为 `.browserlistrc` + +另一方面,如果你单独使用 `yarn ng update` 命令检查首先要更新哪些包会更好. Angular会给你一个要更新的包列表. + +![Table of packages to update](./images/table-of-packages-to-update.png) + +当Angular报告上面的包后,运行命令: + +```sh +yarn ng update @angular/cli @angular/core ng-zorro-antd --force +``` + +> 如果Angular提示你的仓库有中未提交的更改,可以提交/存储它,也可以在命令中添加 `--allow-dirty` 参数. + +### 配置模块 + +在ABP v2.x中,每个延迟加载的模块都有一个可通过单独的程序包使用的配置模块,模块配置如下: + +```js +import { AccountConfigModule } from '@abp/ng.account.config'; + +@NgModule({ + imports: [ + // other imports + AccountConfigModule.forRoot({ redirectUrl: '/' }), + ], + // providers, declarations, and bootstrap +}) +export class AppModule {} +``` + +...在app-routing.module.ts... + +```js +const routes: Routes = [ + // other route configuration + { + path: 'account', + loadChildren: () => import( + './lazy-libs/account-wrapper.module' + ).then(m => m.AccountWrapperModule), + }, +]; +``` + +虽然有效,但有一些缺点: + +- 每个模块都有两个独立的程序包,但实际上这些程序包是相互依赖的. +- 配置延迟加载的模块需要包装模块. +- ABP Commercial具有可扩展性系统,在根模块上配置可扩展模块会增加 bundle 的大小. + +在ABP v3.0中,我们为每个配置模块引入了辅助入口点,并且提供了一种在没有包装的情况下配置延迟加载的模块的新方法. 现在模块配置如下所示: + +```js +import { AccountConfigModule } from '@abp/ng.account/config'; + +@NgModule({ + imports: [ + // other imports + AccountConfigModule.forRoot(), + ], + // providers, declarations, and bootstrap +}) +export class AppModule {} +``` + +... 在app-routing.module.ts... + +```js +const routes: Routes = [ + // other route configuration + { + path: 'account', + loadChildren: () => import('@abp/ng.account') + .then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), + }, +]; +``` + +这项更改帮助我们减少了捆绑包的大小并大大缩短了构建时间. 我们相信你会注意到你的应用程序有所不同. + +#### 一个更好的例子 + +AppModule: + +```js +import { AccountConfigModule } from '@abp/ng.account/config'; +import { CoreModule } from '@abp/ng.core'; +import { IdentityConfigModule } from '@abp/ng.identity/config'; +import { SettingManagementConfigModule } from '@abp/ng.setting-management/config'; +import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config'; +import { ThemeBasicModule } from '@abp/ng.theme.basic'; +import { ThemeSharedModule } from '@abp/ng.theme.shared'; +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgxsModule } from '@ngxs/store'; +import { environment } from '../environments/environment'; +import { AppRoutingModule } from './app-routing.module'; + +@NgModule({ + imports: [ + BrowserModule, + BrowserAnimationsModule, + AppRoutingModule, + CoreModule.forRoot({ + environment, + sendNullsAsQueryParam: false, + skipGetAppConfiguration: false, + }), + ThemeSharedModule.forRoot(), + AccountConfigModule.forRoot(), + IdentityConfigModule.forRoot(), + TenantManagementConfigModule.forRoot(), + SettingManagementConfigModule.forRoot(), + ThemeBasicModule.forRoot(), + NgxsModule.forRoot(), + ], + // providers, declarations, and bootstrap +}) +export class AppModule {} +``` + +AppRoutingModule: + +```js +import { DynamicLayoutComponent } from '@abp/ng.core'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: '', + component: DynamicLayoutComponent, + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => import('./home/home.module') + .then(m => m.HomeModule), + }, + { + path: 'account', + loadChildren: () => import('@abp/ng.account') + .then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), + }, + { + path: 'identity', + loadChildren: () => import('@abp/ng.identity') + .then(m => m.IdentityModule.forLazy()), + }, + { + path: 'tenant-management', + loadChildren: () => import('@abp/ng.tenant-management') + .then(m => m.TenantManagementModule.forLazy()), + }, + { + path: 'setting-management', + loadChildren: () => import('@abp/ng.setting-management') + .then(m => m.SettingManagementModule.forLazy()), + }, + ], + }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule {} +``` + +> 你可能已经注意到我们在top级别路由组件上使用了 `DynamicLayoutComponent`. 我们这样做是为了避免不必要的渲染和闪烁. 这不是强制的,但是我们建议在你的应用程序路由中做同样的事情. + +#### 如何迁移? + +- 使用 `yarn remove` 删除你的项目的配置包. +- 从辅助入口点(例如`@abp/ng.identity/config`)导入配置模块. +- 调用所有新配置模块的静态 `forRoot`方法,即使配置没有被传递. +- 调用 `ThemeBasicModule` 的静态 `forRoot` 方法(或商业上的 `ThemeLeptonModule`),并从导入中删除 `SharedModule`(除非已在其中添加了根模块所需的任何内容). +- 在app路由模块中直接导入延迟ABP模块 (如 `() => import('@abp/ng.identity').then(...)`). +- 在所有延迟模块 `then` 中调用的静态 `forLazy` 方法,即使配置没有被传递. +- [可选]使用 `DynamicLayoutComponent` 添加空的父路由,获得更好的性能和UX. + +### RoutesService + +在ABP v2.x中,通过以下两种方式之一将路由添加到菜单: + +- [通过 `AppRoutingModule` 的 `routes` 属性](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule) +- [通过 ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate) + +从v3.0开始,我们更改了添加和修改路由的方式. 我们不再将路由存储在 `ConfigState`中(破坏性更改). 而是有一个名为 `RoutesService` 的新服务,该服务用于添加,修补或删除菜单项. 详情请查看[文档](Modifying-the-Menu.md). + +#### 如何迁移? + +- 检查你是否曾经使用 `ConfigState` 或 `ConfigStateService` 添加路由. 请用 `RoutesService` 的 `add` 方法替换它们. +- 检查你是否曾经修补的路由. 将其替换为 `RoutesService` 的 `patch` 方法. +- 仔细检查你是否使用绝对路径,并在 `add` 或 `patch` 方法调用中为子菜单项提供 `parentName` 而不是 `children` 属性. + +### NavItemsService + +在ABP v2.x中,[通过LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu)添加导航元素. + +从v3.0开始,我们改变了添加和修改导航项的方式,以前的方法不再可用(破坏性更改). 详情请查看[文档](Modifying-the-Menu.md). + +#### 如何迁移? + +- 用 `NavItemsService` 的 `addItems` 方法替换所有 `dispatchAddNavigationElement` 调用. + +### ngx-datatable + +在v3之前,我们一直使用自定义组件 `abp-table` 作为默认表. 但是数据表是复杂的组件,要实现功能齐全的数据表需要大量的精力,我们计划将其引入其他功能. + +从ABP v3开始,我们已切换到经过严格测试,执行良好的数据表格:[ngx-datatable](https://github.com/swimlane/ngx-datatable). 所有的ABP模块都已经实现了ngx-datatable. `ThemeSharedModule` 已经导出了 `NgxDatatableModule`. 因此如果你在终端运行 `yarn add @swimlane/ngx-datatable` 来安装这个包,它将在你的应用的所有模块中可用. + +为了正确设置样式,你需要在angular.json文件的样式部分中添加以下内容: + +```json + { + "input": "node_modules/@swimlane/ngx-datatable/index.css", + "inject": true, + "bundleName": "ngx-datatable-index" + }, + { + "input": "node_modules/@swimlane/ngx-datatable/assets/icons.css", + "inject": true, + "bundleName": "ngx-datatable-icons" + }, + { + "input": "node_modules/@swimlane/ngx-datatable/themes/material.css", + "inject": true, + "bundleName": "ngx-datatable-material" + } +``` + +由于尚未删除 `abp-table`, 因此以前由ABP v2.x构建的模块不会突然丢失所有. 但是它们的外观与内置ABP v3模块有所不同, 因此你可能希望将这些模块中的表转换为ngx-datatable. 为了减少将abp-table转换为ngx-datatable所需的工作量,我们修改了 `ListService` 以使其与 `ngx-datatable` 一起很好地工作,并引入了两个新指令: `NgxDatatableListDirective` 和 `NgxDatatableDefaultDirective`. + +这些指令的用法很简单: + +```js +@Component({ + providers: [ListService], +}) +export class SomeComponent { + data$ = this.list.hookToQuery( + query => this.dataService.get(query) + ); + + constructor( + public readonly list: ListService, + public readonly dataService: SomeDataService, + ) {} +} +``` + +...在组件模板... + +```html + + + +``` + +通过 `NgxDatatableListDirective` 绑定注入的 `ListService` 实例后,你不再需要担心分页或排序. 同样 `NgxDatatableDefaultDirective` 去除了几个属性绑定,以使ngx-datatable适合我们的样式. + +#### 一个更好的例子 + +```html + + + + + + + + + + + + + + +
{%{{{ row.someOtherProp }}}%}
+
+
+
+``` + +#### 如何迁移? + +- 安装 `@swimlane/ngx-datatable` 包. +- 添加ngx-datatable样式到angular.json文件. +- 如果可以的话,根据上面的例子更新你的模. +- 如果你稍后需要这样做,并且打算保留abp-table一段时间,请确保根据此处描述的[破坏性更改](List-Service.md)更新分页. + +**重要说明:**abp-table没有被删除,但已被弃用并在以后的版本中删除. 请考虑切换到ngx-datatable。 + +### 过时的接口 + +某些接口早已被标记为已弃用,现在已将其删除. + +#### 如何迁移? + +请检查你是否仍在使用[Issue中列出的任何内容](https://github.com/abpframework/abp/issues/4281). + +## 下一步是什么? + +* [服务代理](Service-Proxies.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md b/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md index ec3297ea78..1f4e626bd3 100644 --- a/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md +++ b/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md @@ -1,6 +1,5 @@ # 修改菜单 - 菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局. @@ -24,7 +23,97 @@ export const environment = { ## 如何添加导航元素 -### 通过 AppRoutingModule 中的 `routes` 属性 +### 通过 `RoutesService` + +你可以通过调用 `RoutesService` 的 `add` 方法添加路由到菜单,它是一个单例的服务,在root中提供,你可以立即注入使用它. + +```js +import { RoutesService, eLayoutType } from '@abp/ng.core'; +import { Component } from '@angular/core'; + +@Component(/* component metadata */) +export class AppComponent { + constructor(routes: RoutesService) { + routes.add([ + { + path: '/your-path', + name: 'Your navigation', + order: 101, + iconClass: 'fas fa-question-circle', + requiredPolicy: 'permission key here', + layout: eLayoutType.application, + }, + { + path: '/your-path/child', + name: 'Your child navigation', + parentName: 'Your navigation', + order: 1, + requiredPolicy: 'permission key here', + }, + ]); + } +} +``` + +另一种方法是使用路由提供程序. 首先创建一个提供程序: + +```js +// route.provider.ts +import { RoutesService, eLayoutType } from '@abp/ng.core'; +import { APP_INITIALIZER } from '@angular/core'; + +export const APP_ROUTE_PROVIDER = [ + { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, +]; + +function configureRoutes(routes: RoutesService) { + return () => { + routes.add([ + { + path: '/your-path', + name: 'Your navigation', + requiredPolicy: 'permission key here', + order: 101, + iconClass: 'fas fa-question-circle', + layout: eLayoutType.application, + }, + { + path: '/your-path/child', + name: 'Your child navigation', + parentName: 'Your navigation', + requiredPolicy: 'permission key here', + order: 1, + }, + ]); + }; +} +``` + +...然后在app.module.ts ... + +```js +import { NgModule } from '@angular/core'; +import { APP_ROUTE_PROVIDER } from './route.provider'; + +@NgModule({ + providers: [APP_ROUTE_PROVIDER], + // imports, declarations, and bootstrap +}) +export class AppModule {} +``` + +下面是每个属性的工作原理: + +- `path` 是导航元素的绝对路径. +- `name` 是导航元素的label. 可以使用本地化Key和本地化对象. +- `parentName` 是菜单中父路由的 `name` 的引用,用于创建多级菜单项. +- `requiredPolicy` 是用于访问该页面的权限Key. 参阅[权限管理文档](Permission-Management.md). +- `order` 是导航元素的排序. `Administration` 的顺序是 `100`. 在排序top级别菜单项时请记得这一点. +- `iconClass` 是 `i` 标签的class, 它放在导航label的左边. +- `layout` 定义路由使用哪个布局加载. (默认: `eLayoutType.empty`). +- `invisible` 使该项在菜单中不可见. (默认: `false`). + +### 通过 `AppRoutingModule` 的 `routes` 属性 你可以通过在 `app-routing.module` 中将路由作为子属性添加到路由配置的 `data` 属性来定义路由. `@abp/ng.core` 包组织路由并将其存储在 `ConfigState` 中.`ApplicationLayoutComponent` 从存储中获取路由显示在菜单上. @@ -36,7 +125,7 @@ export const environment = { data: { routes: { name: 'Your navigation', - order: 3, + order: 101, iconClass: 'fas fa-question-circle', requiredPolicy: 'permission key here', children: [ @@ -47,100 +136,77 @@ export const environment = { requiredPolicy: 'permission key here', }, ], - } as ABP.Route, // can be imported from @abp/ng.core - } + }, + }, } ``` -- `name` 是导航元素的标签,可以传递本地化密钥或本地化对象. -- `order` 排序导航元素. -- `iconClass` 是 `i` 标签的类,在导航标签的左侧. -- `requiredPolicy` 是访问页面所需的权限key. 参阅 [权限管理文档](./Permission-Management.md) -- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property. -- `children` 是一个数组,用于声明子菜单,它基于给定的 `path` 属性,路径是在`/your-path/child`. - -添加了上面描述的route属性后,导航菜单如下图所示: - -![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png) - -## 通过 ConfigState - -`ConfigStateService` 的 `dispatchAddRoute` 方法可以向菜单添加一个新的导航元素. - -```js -// this.config is instance of ConfigStateService - -const newRoute: ABP.Route = { - name: 'My New Page', - iconClass: 'fa fa-dashboard', - path: 'page', - invisible: false, - order: 2, - requiredPolicy: 'MyProjectName.MyNewPage', -} as Omit; - -this.config.dispatchAddRoute(newRoute); -// returns a state stream which emits after dispatch action is complete -``` - -`newRoute` 放在根级别,没有任何父路由,url将为`/path`. - -如果你想 **添加子路由, 你可以这样做:** +或者你可以这样做: ```js -// this.config is instance of ConfigStateService -// eIdentityRouteNames enum can be imported from @abp/ng.identity - -const newRoute: ABP.Route = { - parentName: eIdentityRouteNames.IdentityManagement, - name: 'My New Page', - iconClass: 'fa fa-dashboard', - path: 'page', - invisible: false, - order: 3, - requiredPolicy: 'MyProjectName.MyNewPage' -} as Omit; - -this.config.dispatchAddRoute(newRoute); -// returns a state stream which emits after dispatch action is complete +{ + path: 'your-path', + data: { + routes: [ + { + path: '/your-path', + name: 'Your navigation', + order: 101, + iconClass: 'fas fa-question-circle', + requiredPolicy: 'permission key here', + }, + { + path: '/your-path/child', + name: 'Your child navigation', + parentName: 'Your navigation', + order: 1, + requiredPolicy: 'permission key here', + }, + ] as ABP.Route[], // can be imported from @abp/ng.core + }, +} ``` -`newRoute` 做为 `eIdentityRouteNames.IdentityManagement` 的子路由添加, url 设置为 `'/identity/page'`. +第二种方法的优点是你不必绑定到父/子结构,可以使用任何喜欢的路由. -新路由看起来像这样: +如上所述添加 `routes` 属性后,导航菜单看起来像这样: -![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png) +![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png) -## 如何修改一个导航元素 +## 如何修补或删除导航元素 -`DispatchPatchRouteByName` 方法通过名称查找路由,并使用二个参数传递的新配置替换存储中的配置. +`RoutesService` 的 `patch` 方法通过名称查找路由,并将配置替换为第二个参数传递的新配置. `remove` 方法会找到一个路由并将其连同其子路由一起删除. ```js -// this.config is instance of ConfigStateService -// eIdentityRouteNames enum can be imported from @abp/ng.identity +// this.routes is instance of RoutesService +// eThemeSharedRouteNames enum can be imported from @abp/ng.theme.shared + +const dashboardRouteConfig: ABP.Route = { + path: '/dashboard', + name: '::Menu:Dashboard', + parentName: '::Menu:Home', + order: 1, + layout: eLayoutType.application, +}; -const newRouteConfig: Partial = { +const newHomeRouteConfig: Partial = { iconClass: 'fas fa-home', - parentName: eIdentityRouteNames.Administration, + parentName: eThemeSharedRouteNames.Administration, order: 0, - children: [ - { - name: 'Dashboard', - path: 'dashboard', - }, - ], }; -this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig); -// returns a state stream which emits after dispatch action is complete +this.routes.add([dashboardRouteConfig]); +this.routes.patch('::Menu:Home', newHomeRouteConfig); +this.routes.remove(['Your navigation']); ``` -* 根据给定的 `parentName` 将 _Home_ 导航移动到 _Administration_ 下拉框下. -* 添加了 icon. -* 指定了顺序. -* 添加了名为 _Dashboard_ 的子路由. +- 根据给定的 `parentName` 将 _Home_ 导航移动到 _Administration_ 下拉菜单下. +- 为 _Home_ 添加了图标. +- 指定 _Home_ 的顺序为列表的第一项. +- 为 _Home_ 添加了一个名为 _Dashboard_ 的子路由. +- 删除 _Your navigation_ 与其子路由. -修改后,导航元素看起来像这样: +上述操作后,新的菜单看起来如下: ![navigation-menu-after-patching](./images/navigation-menu-after-patching.png) @@ -184,7 +250,7 @@ export class AppComponent { } ``` -上面我们在菜单添加了一个搜索输入,最终UI如下:s +上面我们在菜单添加了一个搜索输入,最终UI如下: ![navigation-menu-search-input](./images/navigation-menu-search-input.png) diff --git a/docs/zh-Hans/UI/Angular/images/table-of-packages-to-update.png b/docs/zh-Hans/UI/Angular/images/table-of-packages-to-update.png new file mode 100644 index 0000000000..fc492d3740 Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/table-of-packages-to-update.png differ diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md index ceaa3d3d69..5ec349b8b6 100644 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -1,3 +1,277 @@ -## Dynamic Forms +# 动态表单 -目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file +`提示:` 在开始阅读本文档之前,请确保你已经看过并理解了[abp表单元素](Form-elements.md)文档. + +## 介绍 + +`abp-dynamic-form` 为给定c#模型创建bootstrap表单. + +基本用法: + +````xml + +```` + +Model: + +````csharp +public class DynamicFormsModel : PageModel +{ + [BindProperty] + public DetailedModel MyDetailedModel { get; set; } + + public List CountryList { get; set; } = new List + { + new SelectListItem { Value = "CA", Text = "Canada"}, + new SelectListItem { Value = "US", Text = "USA"}, + new SelectListItem { Value = "UK", Text = "United Kingdom"}, + new SelectListItem { Value = "RU", Text = "Russia"} + }; + + public void OnGet() + { + MyDetailedModel = new DetailedModel + { + Name = "", + Description = "Lorem ipsum dolor sit amet.", + IsActive = true, + Age = 65, + Day = DateTime.Now, + MyCarType = CarType.Coupe, + YourCarType = CarType.Sedan, + Country = "RU", + NeighborCountries = new List() { "UK", "CA" } + }; + } + + public class DetailedModel + { + [Required] + [Placeholder("Enter your name...")] + [Display(Name = "Name")] + public string Name { get; set; } + + [TextArea(Rows = 4)] + [Display(Name = "Description")] + [InputInfoText("Describe Yourself")] + public string Description { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Is Active")] + public bool IsActive { get; set; } + + [Required] + [Display(Name = "Age")] + public int Age { get; set; } + + [Required] + [Display(Name = "My Car Type")] + public CarType MyCarType { get; set; } + + [Required] + [AbpRadioButton(Inline = true)] + [Display(Name = "Your Car Type")] + public CarType YourCarType { get; set; } + + [DataType(DataType.Date)] + [Display(Name = "Day")] + public DateTime Day { get; set; } + + [SelectItems(nameof(CountryList))] + [Display(Name = "Country")] + public string Country { get; set; } + + [SelectItems(nameof(CountryList))] + [Display(Name = "Neighbor Countries")] + public List NeighborCountries { get; set; } + } + + public enum CarType + { + Sedan, + Hatchback, + StationWagon, + Coupe + } +} +```` + +## Demo + +参阅 [动态表单demo页面](https://bootstrap-taghelpers.abp.io/Components/Dropdowns)查看示例. + +## Attributes + +### abp-model + +为动态表单设置c#模型,模型的属性以表单形式转化为输入. + +### submit-button + +可以为 `True` 或 `False`. + +如果为 `True`,则会在表单底部生成一个提交按钮. + +默认值是 `False`. + +### required-symbols + +可以为 `True` 或 `False`. + +如果为 `True`,则必需的输入将带有一个符号(*),表示它们是必需的. + +默认值是 `True`. + +## 表单内容布局 + +默认情况下,“`abp-dynamic-form` 会清除内部html并将inputs放入自身. 如果要向动态表单添加其他内容或将inputs放置到某些特定区域,可以使用``标签. 这个标签将被表单内容替换, 而 `abp-dynamic-form` 标签的内部html的其余部分将保持不变. + +用法: + +````xml + +
+ Some content.... +
+
+ +
+
+ Some more content.... +
+
+```` + +## 输入排序 + +`abp-dynamic-form` 通过 `DisplayOrder` attribute对属性进行排序,然后按模型类中的属性顺序进行排序. + +默认每个属性的 `DisplayOrder` attribute值是10000. + +参见以下示例: + +````csharp +public class OrderExampleModel +{ + [DisplayOrder(10004)] + public string Name{ get; set; } + + [DisplayOrder(10005)] + public string Surname{ get; set; } + + //Default 10000 + public string EmailAddress { get; set; } + + [DisplayOrder(10003)] + public string PhoneNumber { get; set; } + + [DisplayOrder(9999)] + public string City { get; set; } +} +```` + +在这个示例中,inputs字段顺序为: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`. + +## 忽略属性 + +默认情况下, `abp-dynamic-form` 会为模型类中的每个属性生成输入. 如果要忽略属性请使用 `DynamicFormIgnore` attribute. + +参见以下示例: + +````csharp +public class MyModel +{ + public string Name { get; set; } + + [DynamicFormIgnore] + public string Surname { get; set; } +} +```` + +在这个示例中,不会为 `Surname` 属性生成输入. + +## 指示文本框,单选按钮组和组合框 + +如果你已经阅读了[表单元素文档](Form-elements.md),你会注意到在c#模型上 `abp-radio` 和 `abp-select` 标签非常相. 我们必须使用 `[AbpRadioButton()]` attribute来告诉 `abp-dynamic-form` 你的哪些属性是单选按钮组,哪些属性是组合框. + +参见以下示例: + +````xml + +```` + +Model: + +````csharp +public class DynamicFormsModel : PageModel +{ + [BindProperty] + public DetailedModel MyDetailedModel { get; set; } + + public List CountryList { get; set; } = new List + { + new SelectListItem { Value = "CA", Text = "Canada"}, + new SelectListItem { Value = "US", Text = "USA"}, + new SelectListItem { Value = "UK", Text = "United Kingdom"}, + new SelectListItem { Value = "RU", Text = "Russia"} + }; + + public void OnGet() + { + MyDetailedModel = new DetailedModel + { + ComboCarType = CarType.Coupe, + RadioCarType = CarType.Sedan, + ComboCountry = "RU", + RadioCountry = "UK" + }; + } + + public class DetailedModel + { + public CarType ComboCarType { get; set; } + + [AbpRadioButton(Inline = true)] + public CarType RadioCarType { get; set; } + + [SelectItems(nameof(CountryList))] + public string ComboCountry { get; set; } + + [AbpRadioButton()] + [SelectItems(nameof(CountryList))] + public string RadioCountry { get; set; } + } + + public enum CarType + { + Sedan, + Hatchback, + StationWagon, + Coupe + } +} +```` + +正如你上面的例子中看到: + +* 如果在**Enum**属性上使用 `[AbpRadioButton()]`,它将是一个单选按钮组. 否则它是组合框. +* 如果在属性上使用 `[SelectItems()]` 和 `[AbpRadioButton()]`,那么它将是一个单选按钮组. +* 如果只在属性上使用 `[SelectItems()]`,它将是一个组合框. +* 如果一个属性没有使用这些属性,它将是一个文本框. + +## 本地化 + +`abp-dynamic-form` 会处理本地化. + +默认情况下, 它将尝试查找 "DisplayName:{PropertyName}" 或 "{PropertyName}" 定位本地化键,并将定位值设置为label. + +你可以使用Asp.Net Core的 `[Display()]` attribute自行设置. 可以在此属性中使用本地化密钥. 请参阅以下示例: + +````csharp +[Display(Name = "Name")] +public string Name { get; set; } +```` \ No newline at end of file diff --git a/docs/zh-Hans/Unit-Of-Work.md b/docs/zh-Hans/Unit-Of-Work.md index 233467250b..0fbf5762f0 100644 --- a/docs/zh-Hans/Unit-Of-Work.md +++ b/docs/zh-Hans/Unit-Of-Work.md @@ -325,7 +325,7 @@ public async Task CreateAsync(string name) 如果你的目的只是在创建/更新/删除实体后保存更改,建议你使用 `autoSave` 选项,而不是手动使用 `CurrentUnitOfWork.SaveChangesAsync()`. -> **Note-1**: 当工作单元结束而没有任何错误时,所有更改都会自动保存. 所以除非确实需要,否则不要调用 `SaveChangesAsync()`. +> **Note-1**: 当工作单元结束而没有任何错误时,所有更改都会自动保存. 所以除非确实需要,否则不要调用 `SaveChangesAsync()` 和设置 `autoSave` 为 `true`. > > **Note-2**: 如果你使用 `Guid` 作为主键,则无需插入时保存来获取生成的id,因为 `Guid` 主键是在应用程序中设置的,创建新实体后立即可用. diff --git a/docs/zh-Hans/Virtual-File-System.md b/docs/zh-Hans/Virtual-File-System.md index 3f1e9c2236..6952c4d5da 100644 --- a/docs/zh-Hans/Virtual-File-System.md +++ b/docs/zh-Hans/Virtual-File-System.md @@ -2,109 +2,77 @@ 虚拟文件系统使得管理物理上不存在于文件系统中(磁盘)的文件成为可能. 它主要用于将(js, css, image...)文件嵌入到程序集中, 并在运行时将它们象物理文件一样使用. -### Volo.Abp.VirtualFileSystem nuget包 +## 安装 -Volo.Abp.VirtualFileSystem是虚拟文件系统的核心包. 使用程序包管理器控制台(PMC)将其安装到项目中: +> 大多数情况下你不需要手动安装这个包,因为[应用程序启动模板](Startup-Templates/Application.md)已经预先安装. -``` -Install-Package Volo.Abp.VirtualFileSystem -``` +[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) 是虚拟文件系统的NuGet主页. -> 启动模板默认已经安装了此nuget包, 所以在大多数情况下你不需要手动安装它. +使用ABP CLIi添加包到你的项目: -然后你可以在module中添加 **AbpVirtualFileSystemModule** 依赖项: +* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装. +* 在你想要添加 `Volo.Abp.VirtualFileSystem` 包的项目的 `.csproj` 文件目录打开命令行(终端). +* 运行 `abp add-package Volo.Abp.VirtualFileSystem` 命令. -```c# -using Volo.Abp.Modularity; -using Volo.Abp.VirtualFileSystem; +如果你想要手动安装,安装[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem)NuGet包到你的项目并且添加`[DependsOn(typeof(AbpVirtualFileSystemModule))]`到你项目的[ABP Module](Module-Development-Basics.md)类. -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpVirtualFileSystemModule))] - public class MyModule : AbpModule - { - //... - } -} -``` +## 与嵌入式文件工作 -#### 注册嵌入式文件 +### 嵌入文件 -要将文件嵌入到程序集中, 首先需要把该文件标记为嵌入式资源. 最简单的方式是在 **解决方案管理器** 中选择文件, 然后找到 **"属性"** 窗口将 **"生成操作"** 设置为 **"嵌入式资源"**. +要将文件嵌入到程序集中, 首先需要把该文件标记为**嵌入式资源**. 最简单的方式是在 **解决方案管理器** 中选择文件, 然后找到 **"属性"** 窗口将 **"生成操作"** 设置为 **"嵌入式资源"**. 例如: ![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png) 如果需要添加多个文件, 这样做会很乏味. 作为选择, 你可以直接编辑 **.csproj** 文件: -````C# - - - - -```` +如果文件名包含一些特殊字符,在项目/程序集中嵌入文件可能会导致问题. 为了克服这个限制; + +1. 将[Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet包添加到包含嵌入式资源的项目中. +2. 添加 `true` 到 `.csproj` 文件的 `...` 部分中. 此配置以递归方式添加项目的 **MyResources** 文件夹下的所有文件(包括将来新添加的文件). -然后需要使用 `AbpVirtualFileSystemOptions` 来配置模块, 以便将嵌入式文件注册到虚拟文件系统. 例如: +> 尽管这两个步骤是可选的,并且ABP无需这些配置即可工作,但强烈建议你这样做. -````C# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; -using Volo.Abp.VirtualFileSystem; +### 配置AbpVirtualFileSystemOptions -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpVirtualFileSystemModule))] - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - //Register all embedded files of this assembly to the virtual file system - options.FileSets.AddEmbedded("YourRootNameSpace"); - }); +使用 `AbpVirtualFileSystemOptions` [选项类](Options.md)可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中将嵌入式文件注册到虚拟文件系统. - //... - } - } -} -```` - -`AddEmbedded` 扩展方法需要一个类, 从给定类的程序集中查找所有嵌入文件, 并将它们注册到虚拟文件系统. 它还有更简洁的写法: +**示例: 添加嵌入式文件到虚拟文件系统** -````C# -options.FileSets.Add(new EmbeddedFileSet(typeof(MyModule).Assembly), "YourRootNameSpace"); +````csharp +Configure(options => +{ + options.FileSets.AddEmbedded(); +}); ```` -> "YourRootNameSpace" 是项目的根命名空间名字. 如果你的项目的根命名空间名字为空,则无需传递此参数. +`AddEmbedded` 扩展方法需要一个类, 从给定**类的程序集中查找所有嵌入文件**, 并将它们注册到虚拟文件系统. 通常将模块类作为通用参数传递. -#### 获取虚拟文件: IVirtualFileProvider +`AddEmbedded` 有两个可选参数; -将文件嵌入到程序集中并注册到虚拟文件系统后, 可以使用`IVirtualFileProvider`来获取文件或目录内容: +* `baseNamespace`: 仅在你没有配置上面说明的 `GenerateEmbeddedFilesManifest` 并且你的根名称空间不为空时,才可能需要这样做. 在这种情况下,请在此处设置你的根名称空间. +* `baseFolder`: 如果你不想公开项目中的所有嵌入式文件,只希望公开特定的文件夹(和子文件夹/文件),可以相对于项目根页面设置基本文件夹. -````C# -public class MyService -{ - private readonly IVirtualFileProvider _virtualFileProvider; +**示例: 添加项目中 `MyFiles` 目录下的文件** - public MyService(IVirtualFileProvider virtualFileProvider) - { - _virtualFileProvider = virtualFileProvider; - } +````csharp +Configure(options => +{ + options.FileSets.AddEmbedded( + baseNamespace: "Acme.BookStore.MyFiles", + baseFolder: "/MyFiles" + ); +}); +```` - public void Foo() - { - //Getting a single file - var file = _virtualFileProvider.GetFileInfo("/MyResources/js/test.js"); - var fileContent = file.ReadAsString(); //ReadAsString is an extension method of ABP +这个例子假设; - //Getting all files/directories under a directory - var directoryContents = _virtualFileProvider.GetDirectoryContents("/MyResources/js"); - } -} -```` +* 你的项目根(default)命令空间是 `Acme.BookStore`. +* 你的项目有一个名为 `MyFiles` 的目录. +* 你只想添加 `MyFiles` 目录到虚拟文件系统. #### 在开发过程中处理嵌入式文件 @@ -128,27 +96,61 @@ public class MyWebAppModule : AbpModule { Configure(options => { - //ReplaceEmbeddedByPhysical gets the root folder of the MyModule project options.FileSets.ReplaceEmbeddedByPhysical( - Path.Combine(hostingEnvironment.ContentRootPath, "..\\MyModuleProject") + Path.Combine( + hostingEnvironment.ContentRootPath, + string.Format( + "..{0}MyModuleProject", + Path.DirectorySeparatorChar + ) + ) ); }); } - - //... } } ```` 上面的代码假设`MyWebAppModule`和`MyModule`是Visual Studio解决方案中的两个不同的项目, `MyWebAppModule`依赖于`MyModule`. +> [应用程序启动模板]已经为本地化文件应用这个方法,所以当你更改一个本地化文件时,它会自动检测到更改. + +## IVirtualFileProvider + +将文件嵌入到程序集中并注册到虚拟文件系统后,可以使用 `IVirtualFileProvider` 接口来获取文件或目录内容: + +````C# +public class MyService +{ + private readonly IVirtualFileProvider _virtualFileProvider; + + public MyService(IVirtualFileProvider virtualFileProvider) + { + _virtualFileProvider = virtualFileProvider; + } + + public void Foo() + { + //Getting a single file + var file = _virtualFileProvider + .GetFileInfo("/MyResources/js/test.js"); + + var fileContent = file.ReadAsString(); + + //Getting all files/directories under a directory + var directoryContents = _virtualFileProvider + .GetDirectoryContents("/MyResources/js"); + } +} +```` + ### ASP.NET Core 集成 虚拟文件系统与 ASP.NET Core 无缝集成: * 虚拟文件可以像Web应用程序上的物理(静态)文件一样使用. * Js, css, 图像文件和所有其他Web内容可以嵌入到程序集中并像物理文件一样使用. -* 应用程序(或其他模块)可以覆盖模块的虚拟文件, 就像将具有相同名称和扩展名的文件放入虚拟文件的同一文件夹中一样. +* 应用程序(或其他模块)可以**覆盖模块的虚拟文件**, 就像将具有相同名称和扩展名的文件放入虚拟文件的同一文件夹中一样. #### 虚拟文件中间件 @@ -160,4 +162,14 @@ app.UseVirtualFiles(); 在静态文件中间件之后添加虚拟文件中间件, 使得通过在虚拟文件相同的位置放置物理文件, 从而用物理文件覆盖虚拟文件成为可能. -> 虚拟文件中间件可以虚拟wwwroot文件夹中的内容 - 就像静态文件一样. +> [应用程序启动模板](Startup-Templates/Application.md)已经配置了 `UseVirtualFiles()`. + +#### 静态虚拟文件夹 + +默认情况下,ASP.NET Core仅允许 `wwwroot` 文件夹包含客户端使用的静态文件. 当你使用 `UseVirtualFiles` 中间件时以下文件夹也可以包含静态文件: + +* Pages +* Views +* Themes + +这允许你可以在 `.cshtml` 文件附近添加 `.js`, `.css`... 文件,更易于开发和维护你的项目. diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index 04da437b7b..66229da6da 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -368,6 +368,10 @@ { "text": "Angular", "items": [ + { + "text": "v2.x 到 v3 迁移指南", + "path": "UI/Angular/Migration-Guide-v3.md" + }, { "text": "服务代理", "path": "UI/Angular/Service-Proxies.md"