@ -0,0 +1,53 @@ |
|||||
|
# ABP.IO Platform 4.2 Final Has Been Released! |
||||
|
|
||||
|
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 4.2 versions have been released today. |
||||
|
|
||||
|
## What's New With 4.2? |
||||
|
|
||||
|
Since all the new features are already explained in details with the [4.2 RC Announcement Post](https://blog.abp.io/abp/ABP-IO-Platform-v4-2-RC-Has-Been-Released), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP-IO-Platform-v4-2-RC-Has-Been-Released) for all the features and enhancements. |
||||
|
|
||||
|
## Creating New Solutions |
||||
|
|
||||
|
You can create a new solution with the ABP Framework version 4.2 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). |
||||
|
|
||||
|
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details. |
||||
|
|
||||
|
## How to Upgrade an Existing Solution |
||||
|
|
||||
|
### Install/Update the ABP CLI |
||||
|
|
||||
|
First of all, install the ABP CLI or upgrade to the latest version. |
||||
|
|
||||
|
If you haven't installed yet: |
||||
|
|
||||
|
```bash |
||||
|
dotnet tool install -g Volo.Abp.Cli |
||||
|
``` |
||||
|
|
||||
|
To update an existing installation: |
||||
|
|
||||
|
```bash |
||||
|
dotnet tool update -g Volo.Abp.Cli |
||||
|
``` |
||||
|
|
||||
|
### ABP UPDATE Command |
||||
|
|
||||
|
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: |
||||
|
|
||||
|
```bash |
||||
|
abp update |
||||
|
``` |
||||
|
|
||||
|
Run this command in the root folder of your solution. |
||||
|
|
||||
|
## Migration Guide |
||||
|
|
||||
|
Check [the migration guide](https://docs.abp.io/en/abp/latest/Migration-Guides/Abp-4_2) for the applications with the version 4.x upgrading to the version 4.2. |
||||
|
|
||||
|
> It is strongly recommended to check the migration guide for this version. Especially, the new `IRepository.GetQueryableAsync()` method is a core change should be considered after upgrading the solution. |
||||
|
|
||||
|
## About the Next Version |
||||
|
|
||||
|
The next feature version will be 4.3. It is planned to release the 4.3 RC (Release Candidate) on March 11 and the final version on March 25, 2021. |
||||
|
|
||||
|
We decided to slow down the feature development for the [next milestone](https://github.com/abpframework/abp/milestone/49). We will continue to improve the existing features and introduce new ones, sure, but wanted to have more time for the planning, documentation, creating guides and improving the development experience. |
||||
@ -0,0 +1,103 @@ |
|||||
|
## Using MatBlazor UI Components With the ABP Framework |
||||
|
|
||||
|
Hi, in this step by step article, I will show you how to integrate [MatBlazor](https://www.matblazor.com/), a blazor UI components into ABP Framework-based applications. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
*(A screenshot from the example application developed in this article)* |
||||
|
|
||||
|
## Create the Project |
||||
|
|
||||
|
> First thing is to create the project. ABP Framework offers startup templates to get into business faster. |
||||
|
|
||||
|
In this article, I will create a new startup template with EF Core as a database provider and Blazor for UI framework. But if you already have a project with Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project. |
||||
|
|
||||
|
> If you already have a project with the Blazor UI, you can skip this section. |
||||
|
|
||||
|
* Before starting the development, we will create a new solution named `MatBlazorSample` (or whatever you want). We will create a new startup template with EF Core as a database provider and Blazor for UI framework by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): |
||||
|
|
||||
|
````bash |
||||
|
abp new MatBlazorSample -u blazor |
||||
|
```` |
||||
|
|
||||
|
This will create new project inside of `aspnet-core`, so: |
||||
|
|
||||
|
````bash |
||||
|
cd aspnet-core |
||||
|
```` |
||||
|
|
||||
|
and |
||||
|
|
||||
|
````bash |
||||
|
dotnet restore |
||||
|
```` |
||||
|
|
||||
|
* Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE) and run the `MatBlazorSample.DbMigrator` to create the database and seed initial data (which creates the admin user, admin role, permissions etc.) |
||||
|
|
||||
|
 |
||||
|
|
||||
|
* After database and initial data created, |
||||
|
* Run the `MatBlazorSample.HttpApi.Host` to see our server side working and |
||||
|
* Run the `MatBlazorSample.Blazor` to see our UI working properly. |
||||
|
|
||||
|
> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_ |
||||
|
|
||||
|
## Install MatBlazor |
||||
|
|
||||
|
You can follow [this documentation](https://www.matblazor.com/) to install MatBlazor packages into your computer. |
||||
|
|
||||
|
### Adding MatBlazor NuGet Packages |
||||
|
|
||||
|
```bash |
||||
|
Install-Package MatBlazor |
||||
|
``` |
||||
|
|
||||
|
### Register MatBlazor Resources |
||||
|
|
||||
|
1. Add the following line to the HEAD section of the `wwwroot/index.html` file within the `MatBlazorSample.Blazor` project: |
||||
|
|
||||
|
```Razor |
||||
|
<head> |
||||
|
<!--...--> |
||||
|
<script src="_content/MatBlazor/dist/matBlazor.js"></script> |
||||
|
<link href="_content/MatBlazor/dist/matBlazor.css" rel="stylesheet" /> |
||||
|
</head> |
||||
|
``` |
||||
|
|
||||
|
2. In the `MatBlazorSampleBlazorModule` class, call the `AddMatBlazor()` method from your project's `ConfigureServices()` method: |
||||
|
|
||||
|
```csharp |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var environment = context.Services.GetSingletonInstance<IWebAssemblyHostEnvironment>(); |
||||
|
var builder = context.Services.GetSingletonInstance<WebAssemblyHostBuilder>(); |
||||
|
// ... |
||||
|
builder.Services.AddMatBlazor(); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
3. Register the **MatBlazorSample.Blazor** namespace in the `_Imports.razor` file: |
||||
|
|
||||
|
```Razor |
||||
|
@using MatBlazor |
||||
|
``` |
||||
|
|
||||
|
## The Sample Application |
||||
|
|
||||
|
We have created a sample application with [Table](https://www.matblazor.com/Table) example. |
||||
|
|
||||
|
### The Source Code |
||||
|
|
||||
|
You can download the source code from [here](https://github.com/abpframework/abp-samples/tree/master/MatBlazorSample). |
||||
|
|
||||
|
The related files for this example are marked in the following screenshots. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
 |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Conclusion |
||||
|
|
||||
|
In this article, I've explained how to use [MatBlazor](https://www.matblazor.com/) components in your application. ABP Framework is designed so that it can work with any UI library/framework. |
||||
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,343 @@ |
|||||
|
# How to Use PrimeNG Components with the ABP Angular UI |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
In this article, we will use components of the [PrimeNG](https://www.primefaces.org/primeng/) that is a popular UI component library for Angular with the ABP Framework Angular UI that will be generated via [ABP CLI](https://docs.abp.io/en/abp/latest/CLI). |
||||
|
|
||||
|
We will create an organization units page and use PrimeNG's [OrganizationChart](https://primefaces.org/primeng/showcase/#/organizationchart) and [Table](https://primefaces.org/primeng/showcase/#/table) components on the page. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
<small>The UI shown above contains many PrimeNG components. You can reach the source code of this rich UI. Take a look at the source code section below.</small> |
||||
|
|
||||
|
> This article does not cover any backend code. I used mock data to provide data source to the components. |
||||
|
|
||||
|
## Pre-Requirements |
||||
|
|
||||
|
The following tools should be installed on your development machine: |
||||
|
|
||||
|
* [.NET Core 5.0+](https://www.microsoft.com/net/download/dotnet-core/) |
||||
|
* [Node v12 or v14](https://nodejs.org/) |
||||
|
* [VS Code](https://code.visualstudio.com/) or another IDE |
||||
|
|
||||
|
## Source Code |
||||
|
|
||||
|
I have prepared a sample project that contains more PrimeNG components than described in this article. You can download the source code [on GitHub](https://github.com/abpframework/abp-samples/tree/master/PrimengSample). |
||||
|
|
||||
|
## Creating a New Solution |
||||
|
|
||||
|
In this step, we will create a new solution that contains Angular UI and backend startup templates. If you have a startup template with Angular UI, you can skip this step. |
||||
|
|
||||
|
Run the following command to install the ABP CLI: |
||||
|
|
||||
|
```bash |
||||
|
dotnet tool install -g Volo.Abp.Cli |
||||
|
``` |
||||
|
|
||||
|
...or update: |
||||
|
|
||||
|
```bash |
||||
|
dotnet tool update -g Volo.Abp.Cli |
||||
|
``` |
||||
|
|
||||
|
Create a new solution named `AbpPrimengSample` by running the following command: |
||||
|
|
||||
|
```bash |
||||
|
abp new AbpPrimengSample -u angular -csf |
||||
|
``` |
||||
|
|
||||
|
See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all available options. |
||||
|
|
||||
|
You can also use the Direct Download tab on the [Get Started](https://abp.io/get-started) page. |
||||
|
|
||||
|
## Running the Solution |
||||
|
|
||||
|
You can run the solution as described in [here](https://docs.abp.io/en/abp/latest/Getting-Started-Running-Solution?UI=NG&DB=EF&Tiered=No). |
||||
|
|
||||
|
## PrimeNG Setup |
||||
|
|
||||
|
Open the `angular` folder and run the following command to install packages: |
||||
|
|
||||
|
```bash |
||||
|
npm install |
||||
|
``` |
||||
|
|
||||
|
Next, we need to install `primeng` and required packages (`primeicons` and `@angular/cdk`) for the library. Run the command below to install these packages: |
||||
|
|
||||
|
```bash |
||||
|
npm install primeng primeicons @angular/cdk --save |
||||
|
``` |
||||
|
|
||||
|
The packages we have installed; |
||||
|
|
||||
|
- `primeng` is the main package that is a component library. |
||||
|
- `primeicons` is an icon font library. Many PrimeNG components use this font internally. |
||||
|
- `@angular/cdk` is a component dev kit created by the Angular team. Some PrimeNG modules depend on it. |
||||
|
|
||||
|
As the last step of the setup, we should add the required style files for the library to `angular.json`: |
||||
|
|
||||
|
```js |
||||
|
//angular.json |
||||
|
|
||||
|
"projects": { |
||||
|
"AbpPrimengSample": { |
||||
|
//... |
||||
|
"styles": { |
||||
|
"node_modules/primeicons/primeicons.css", |
||||
|
"node_modules/primeng/resources/themes/saga-blue/theme.css", |
||||
|
"node_modules/primeng/resources/primeng.min.css", |
||||
|
//...other styles |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
We have added the `primeng.min.css`, Saga Blue theme's `theme.css`, and `primeicons.css` files to the project. You can choose another theme instead of the Sage Blue. See available themes on the [Get Started](https://www.primefaces.org/primeng/showcase/#/setup) document of the PrimeNG. |
||||
|
|
||||
|
|
||||
|
> You have to restart the running `ng serve` process to see the effect of the changes you made in the `angular.json`. |
||||
|
|
||||
|
|
||||
|
## Creating the Organization Units Page |
||||
|
|
||||
|
Run the following command to create a new module named `OrganizationUnits`: |
||||
|
|
||||
|
```bash |
||||
|
npm run ng -- generate module organization-units --route organization-units --module app.module |
||||
|
``` |
||||
|
|
||||
|
Then open the `src/route.provider.ts` and add a new route as an array element to add a navigation link labeled "Organization Units" to the menu: |
||||
|
|
||||
|
```js |
||||
|
//route.provider.ts |
||||
|
|
||||
|
import { eThemeSharedRouteNames } from '@abp/ng.theme.shared'; |
||||
|
//... |
||||
|
|
||||
|
routesService.add([ |
||||
|
//... |
||||
|
{ |
||||
|
path: '/organization-units', |
||||
|
name: 'Organization Units', |
||||
|
parentName: eThemeSharedRouteNames.Administration, |
||||
|
iconClass: 'fas fa-sitemap', |
||||
|
layout: eLayoutType.application, |
||||
|
}, |
||||
|
]); |
||||
|
``` |
||||
|
|
||||
|
We have created a lazy-loadable module and defined a menu navigation link. We can navigate to the page as shown below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Using the PrimeNG Components |
||||
|
|
||||
|
### Implementing the Organization Chart Component |
||||
|
|
||||
|
When you would like to use any component from PrimeNG, you have to import the component's module to your module. Since we will use the `OrganizationChart` on the organization units page, we need to import `OrganizationChartModule` into `OrganizationUnitsModule`. |
||||
|
|
||||
|
Open the `src/organization-units/organization-units.module.ts` and add the `OrganizationChartModule` to the imports array as shown below: |
||||
|
|
||||
|
```js |
||||
|
import { OrganizationChartModule } from 'primeng/organizationchart'; |
||||
|
//... |
||||
|
|
||||
|
@NgModule({ |
||||
|
//... |
||||
|
imports: [ |
||||
|
//... |
||||
|
OrganizationChartModule |
||||
|
], |
||||
|
}) |
||||
|
export class OrganizationUnitsModule {} |
||||
|
``` |
||||
|
|
||||
|
> Since NGCC need to work in some cases, restarting the `ng serve` process would be good when you import any modules from `primeng` package to your module. |
||||
|
|
||||
|
Let's define a mock data source for the `OrganizationChartComponent` and add the component to the page. |
||||
|
|
||||
|
Open the `src/organization-units/organization-units.component.ts` and add two variables as shown below: |
||||
|
|
||||
|
```js |
||||
|
//... |
||||
|
import { TreeNode } from 'primeng/api'; |
||||
|
|
||||
|
@Component(/* component metadata*/) |
||||
|
export class OrganizationUnitsComponent implements OnInit { |
||||
|
//... |
||||
|
|
||||
|
organizationUnits: TreeNode[] = [ |
||||
|
{ |
||||
|
label: 'Management', |
||||
|
expanded: true, |
||||
|
children: [ |
||||
|
{ |
||||
|
label: 'Selling', |
||||
|
expanded: true, |
||||
|
children: [ |
||||
|
{ |
||||
|
label: 'Customer Relations', |
||||
|
}, |
||||
|
{ |
||||
|
label: 'Marketing', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
label: 'Supporting', |
||||
|
expanded: true, |
||||
|
children: [ |
||||
|
{ |
||||
|
label: 'Buying', |
||||
|
}, |
||||
|
{ |
||||
|
label: 'Human Resources', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
selectedUnit: TreeNode; |
||||
|
``` |
||||
|
|
||||
|
- First variable is `organizationUnits`. It provides mock data source to `OrganizationChartComponent`. |
||||
|
- Second variable is `selectedUnit`. It keeps chosen unit on the chart. |
||||
|
|
||||
|
Then, open the `src/organization-units/organization-units.component.html` and replace the file content with the following: |
||||
|
|
||||
|
```html |
||||
|
<div class="card"> |
||||
|
<div class="card-header"> |
||||
|
<h5>Organization Units</h5> |
||||
|
</div> |
||||
|
<div class="card-body"> |
||||
|
<p-organizationChart |
||||
|
[value]="organizationUnits" |
||||
|
selectionMode="single" |
||||
|
[(selection)]="selectedUnit" |
||||
|
></p-organizationChart> |
||||
|
</div> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
We have implemented the `OrganizationChart`. The final UI looks like below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Implementing the Table Component |
||||
|
|
||||
|
In order to use the `TableComponent`, we have to import the `TableModule` to the `OrganizationUnitsModule`. |
||||
|
|
||||
|
Open the `organization-units.module.ts` and add `TableModule` to the imports array as shown below: |
||||
|
|
||||
|
```js |
||||
|
import { TableModule } from 'primeng/table'; |
||||
|
//... |
||||
|
|
||||
|
@NgModule({ |
||||
|
//... |
||||
|
imports: [ |
||||
|
//... |
||||
|
TableModule |
||||
|
], |
||||
|
}) |
||||
|
export class OrganizationUnitsModule {} |
||||
|
``` |
||||
|
|
||||
|
Open the `organization-units.component.ts` and add a variable named `members` with initial value and add a getter named `tableData` as shown below: |
||||
|
|
||||
|
```js |
||||
|
//... |
||||
|
export class OrganizationUnitsComponent implements OnInit { |
||||
|
//... |
||||
|
|
||||
|
members = [ |
||||
|
{ |
||||
|
fullName: 'John Doe', |
||||
|
username: 'John.Doe', |
||||
|
phone: '+1-202-555-0125', |
||||
|
email: 'john.doe@example.com', |
||||
|
parent: 'Customer Relations', |
||||
|
}, |
||||
|
{ |
||||
|
fullName: 'Darrion Walter', |
||||
|
username: 'Darrion.Walter', |
||||
|
phone: '+1-262-155-0355', |
||||
|
email: 'Darrion_Walter@example.com', |
||||
|
parent: 'Marketing', |
||||
|
}, |
||||
|
{ |
||||
|
fullName: 'Rosa Labadie', |
||||
|
username: 'Rosa.Labadie', |
||||
|
phone: '+1-262-723-2255', |
||||
|
email: 'Rosa.Labadie@example.com', |
||||
|
parent: 'Marketing', |
||||
|
}, |
||||
|
{ |
||||
|
fullName: 'Adelle Hills', |
||||
|
username: 'Adelle.Hills', |
||||
|
phone: '+1-491-112-9011', |
||||
|
email: 'Adelle.Hills@example.com', |
||||
|
parent: 'Buying', |
||||
|
}, |
||||
|
{ |
||||
|
fullName: 'Brian Hane', |
||||
|
username: 'Brian.Hane', |
||||
|
phone: '+1-772-509-1823', |
||||
|
email: 'Brian.Hane@example.com', |
||||
|
parent: 'Human Resources', |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
get tableData() { |
||||
|
return this.members.filter(user => user.parent === this.selectedUnit.label); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
What we have done above? |
||||
|
|
||||
|
- We defined a variable named `members` to provide mock data to the table. |
||||
|
- We have defined a getter named `tableData` to provide filtered data source to the table using `members` variable. |
||||
|
|
||||
|
We are now ready to add the table to the HTML template. |
||||
|
|
||||
|
Open the `organization-units.component.html`, find the `p-organizationChart` tag and place the following code to the bottom of this tag: |
||||
|
|
||||
|
```html |
||||
|
<div class="p-3" *ngIf="selectedUnit"> |
||||
|
<h6>Members of {{ selectedUnit.label }}</h6> |
||||
|
|
||||
|
<p-table [value]="tableData"> |
||||
|
<ng-template pTemplate="header"> |
||||
|
<tr> |
||||
|
<th>Name</th> |
||||
|
<th>Username</th> |
||||
|
<th>Email</th> |
||||
|
<th>Phone</th> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
<ng-template pTemplate="body" let-member> |
||||
|
<tr> |
||||
|
<td>{{ member.fullName }}</td> |
||||
|
<td>{{ member.username }}</td> |
||||
|
<td>{{ member.email }}</td> |
||||
|
<td>{{ member.phone }}</td> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
</p-table> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
We have added a new `div` that contains the `TableComponent`. The table appears when an organization unit is selected. |
||||
|
The table contains 4 columns which are name, username, email, and phone for displaying the members' information. |
||||
|
|
||||
|
After adding the table, the final UI looks like this: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
|
||||
|
## Conclusion |
||||
|
|
||||
|
We have implemented the PrimeNG component library on the ABP Angular UI project and used two components on a page in a short time. You can use any PrimeNG components by following the documentation. The ABP Angular UI will not block you in any case. |
||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 392 KiB |
@ -0,0 +1,101 @@ |
|||||
|
# ABP version 4.2 Migration Guide |
||||
|
|
||||
|
This version has no breaking changes but there is an important change on the repositories that should be applied for your application for an important performance and scalability gain. |
||||
|
|
||||
|
## IRepository.GetQueryableAsync |
||||
|
|
||||
|
`IRepository` interface inherits `IQueryable`, so you can directly use the standard LINQ extension methods, like `Where`, `OrderBy`, `First`, `Sum`... etc. |
||||
|
|
||||
|
**Example: Using LINQ directly over the repository object** |
||||
|
|
||||
|
````csharp |
||||
|
public class BookAppService : ApplicationService, IBookAppService |
||||
|
{ |
||||
|
private readonly IRepository<Book, Guid> _bookRepository; |
||||
|
|
||||
|
public BookAppService(IRepository<Book, Guid> bookRepository) |
||||
|
{ |
||||
|
_bookRepository = bookRepository; |
||||
|
} |
||||
|
|
||||
|
public async Task DoItInOldWayAsync() |
||||
|
{ |
||||
|
//Apply any standard LINQ extension method |
||||
|
var query = _bookRepository |
||||
|
.Where(x => x.Price > 10) |
||||
|
.OrderBy(x => x.Name); |
||||
|
|
||||
|
//Execute the query asynchronously |
||||
|
var books = await AsyncExecuter.ToListAsync(query); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
*See [the documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) if you wonder what is the `AsyncExecuter`.* |
||||
|
|
||||
|
**Beginning from the version 4.2, the recommended way is using `IRepository.GetQueryableAsync()` to obtain an `IQueryable`, then use the LINQ extension methods over it.** |
||||
|
|
||||
|
**Example: Using the new GetQueryableAsync method** |
||||
|
|
||||
|
````csharp |
||||
|
public async Task DoItInNewWayAsync() |
||||
|
{ |
||||
|
//Use GetQueryableAsync to obtain the IQueryable<Book> first |
||||
|
var queryable = await _bookRepository.GetQueryableAsync(); |
||||
|
|
||||
|
//Then apply any standard LINQ extension method |
||||
|
var query = queryable |
||||
|
.Where(x => x.Price > 10) |
||||
|
.OrderBy(x => x.Name); |
||||
|
|
||||
|
//Finally, execute the query asynchronously |
||||
|
var books = await AsyncExecuter.ToListAsync(query); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
ABP may start a database transaction when you get an `IQueryable` (If current [Unit Of Work](https://docs.abp.io/en/abp/latest/Unit-Of-Work) is transactional). In this new way, it is possible to **start the database transaction in an asynchronous way**. Previously, we could not get the advantage of asynchronous while starting the transactions. |
||||
|
|
||||
|
> **The new way has a significant performance and scalability gain. The old usage (directly using LINQ over the repositories) will be removed in the next major version (5.0).** You have a lot of time for the change, but we recommend to immediately take the action since the old usage has a big **scalability problem**. |
||||
|
|
||||
|
### Actions to Take |
||||
|
|
||||
|
* Use the repository's queryable feature as explained before. |
||||
|
* If you've overridden `CreateFilteredQuery` in a class derived from `CrudAppService`, you should override the `CreateFilteredQueryAsync` instead and remove the `CreateFilteredQuery` in your class. |
||||
|
* If you've overridden `WithDetails` in your custom repositories, remove it and override `WithDetailsAsync` instead. |
||||
|
* If you've used `DbContext` or `DbSet` properties in your custom repositories, use `GetDbContextAsync()` and `GetDbSetAsync()` methods instead of them. |
||||
|
|
||||
|
You can re-build your solution and check the `Obsolete` warnings to find some of the usages need to change. |
||||
|
|
||||
|
#### About IRepository Async Extension Methods |
||||
|
|
||||
|
Using IRepository Async Extension Methods has no such a problem. The examples below are pretty fine: |
||||
|
|
||||
|
````csharp |
||||
|
var countAll = await _personRepository |
||||
|
.CountAsync(); |
||||
|
|
||||
|
var count = await _personRepository |
||||
|
.CountAsync(x => x.Name.StartsWith("A")); |
||||
|
|
||||
|
var book1984 = await _bookRepository |
||||
|
.FirstOrDefaultAsync(x => x.Name == "John"); |
||||
|
```` |
||||
|
|
||||
|
See the [repository documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) to understand the relation between `IQueryable` and asynchronous operations. |
||||
|
|
||||
|
## .NET Package Upgrades |
||||
|
|
||||
|
ABP uses the latest 5.0.* .NET packages. If your application is using 5.0.0 packages, you may get an error on build. We recommend to depend on the .NET packages like `5.0.*` in the `.csproj` files to use the latest patch versions. |
||||
|
|
||||
|
Example: |
||||
|
|
||||
|
````xml |
||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.*" /> |
||||
|
```` |
||||
|
|
||||
|
## Blazorise Library Upgrade |
||||
|
|
||||
|
If you are upgrading to 4.2, you also need also upgrade the following packages in your Blazor application; |
||||
|
|
||||
|
* `Blazorise.Bootstrap` to `0.9.3-preview6` |
||||
|
* `Blazorise.Icons.FontAwesome` to `0.9.3-preview6` |
||||
@ -1,5 +1,6 @@ |
|||||
# ABP Framework Migration Guides |
# ABP Framework Migration Guides |
||||
|
|
||||
* [3.3.x to 4.0 Migration Guide](Abp-4_0.md) |
* [4.x to 4.2](Abp-4_2.md) |
||||
* [2.9.x to 3.0 Migration Guide](../UI/Angular/Migration-Guide-v3.md) |
* [3.3.x to 4.0](Abp-4_0.md) |
||||
|
* [2.9.x to 3.0](../UI/Angular/Migration-Guide-v3.md) |
||||
|
|
||||
|
|||||
@ -0,0 +1,83 @@ |
|||||
|
# Ellipsis |
||||
|
|
||||
|
Text inside an HTML element can be truncated easily with an ellipsis by using CSS. To make this even easier, you can use the `EllipsisDirective` which has been exposed by the `@abp/ng.theme.shared` package. |
||||
|
|
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
In order to use the `EllipsisDirective` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this: |
||||
|
|
||||
|
```js |
||||
|
// ... |
||||
|
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
//... |
||||
|
imports: [..., ThemeSharedModule], |
||||
|
}) |
||||
|
export class MyFeatureModule {} |
||||
|
``` |
||||
|
|
||||
|
or **if you would not like to import** the `ThemeSharedModule`, you can import the **`EllipsisModule`** as shown below: |
||||
|
|
||||
|
|
||||
|
```js |
||||
|
// ... |
||||
|
import { EllipsisModule } from '@abp/ng.theme.shared'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
//... |
||||
|
imports: [..., EllipsisModule], |
||||
|
}) |
||||
|
export class MyFeatureModule {} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
The `EllipsisDirective` is very easy to use. The directive's selector is **`abpEllipsis`**. By adding the `abpEllipsis` attribute to an HTML element, you can activate the `EllipsisDirective` for the HTML element. |
||||
|
|
||||
|
See an example usage: |
||||
|
|
||||
|
```html |
||||
|
<p abpEllipsis> |
||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam commodi quae aspernatur, |
||||
|
corporis velit et suscipit id consequuntur amet minima expedita cum reiciendis dolorum |
||||
|
cupiditate? Voluptas eaque voluptatum odio deleniti quo vel illum nemo accusamus nulla ratione |
||||
|
impedit dolorum expedita necessitatibus fugiat ullam beatae, optio eum cupiditate ducimus |
||||
|
architecto. |
||||
|
</p> |
||||
|
``` |
||||
|
|
||||
|
The `abpEllipsis` attribute has been added to the `<p>` element that containing very long text inside to activate the `EllipsisDirective`. |
||||
|
|
||||
|
See the result: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
The long text has been truncated by using the directive. |
||||
|
|
||||
|
The UI before using the directive looks like this: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Specifying Max Width of an HTML Element |
||||
|
|
||||
|
An HTML element max width can be specified as shown below: |
||||
|
|
||||
|
```html |
||||
|
<div [abpEllipsis]="'100px'"> |
||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque, optio! |
||||
|
</div> |
||||
|
|
||||
|
<div [abpEllipsis]="'15vw'"> |
||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque, optio! |
||||
|
</div> |
||||
|
|
||||
|
<div [abpEllipsis]="'50%'"> |
||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque, optio! |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
See the result: |
||||
|
|
||||
|
 |
||||
@ -0,0 +1,288 @@ |
|||||
|
# How Replaceable Components Work with Extensions |
||||
|
|
||||
|
Additional UI extensibility points ([Entity action extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Entity-Action-Extensions), [data table column extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Data-Table-Column-Extensions), [page toolbar extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Page-Toolbar-Extensions) and others) are used in ABP pages to allow to control entity actions, table columns and page toolbar of a page. If you replace a page, you need to apply some configurations to be able to work extension components in your component. Let's see how to do this by replacing the roles page. |
||||
|
|
||||
|
Create a new module called `MyRolesModule`: |
||||
|
|
||||
|
```bash |
||||
|
yarn ng generate module my-roles --module app |
||||
|
``` |
||||
|
|
||||
|
Create a new component called `MyRolesComponent`: |
||||
|
|
||||
|
```bash |
||||
|
yarn ng generate component my-roles/my-roles --flat --export |
||||
|
``` |
||||
|
|
||||
|
Open the generated `src/app/my-roles/my-roles.component.ts` file and replace its content with the following: |
||||
|
|
||||
|
```js |
||||
|
import { ListService, PagedAndSortedResultRequestDto } from '@abp/ng.core'; |
||||
|
import { |
||||
|
CreateRole, |
||||
|
DeleteRole, |
||||
|
eIdentityComponents, |
||||
|
GetRoleById, |
||||
|
GetRoles, |
||||
|
IdentityRoleDto, |
||||
|
IdentityState, |
||||
|
RolesComponent, |
||||
|
UpdateRole, |
||||
|
} from '@abp/ng.identity'; |
||||
|
import { ePermissionManagementComponents } from '@abp/ng.permission-management'; |
||||
|
import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; |
||||
|
import { |
||||
|
EXTENSIONS_IDENTIFIER, |
||||
|
FormPropData, |
||||
|
generateFormFromProps, |
||||
|
} from '@abp/ng.theme.shared/extensions'; |
||||
|
import { Component, Injector, OnInit } from '@angular/core'; |
||||
|
import { FormGroup } from '@angular/forms'; |
||||
|
import { Select, Store } from '@ngxs/store'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { finalize, pluck } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-my-roles', |
||||
|
templateUrl: './my-roles.component.html', |
||||
|
providers: [ |
||||
|
ListService, |
||||
|
{ |
||||
|
provide: EXTENSIONS_IDENTIFIER, |
||||
|
useValue: eIdentityComponents.Roles, |
||||
|
}, |
||||
|
{ provide: RolesComponent, useExisting: MyRolesComponent }, |
||||
|
], |
||||
|
}) |
||||
|
export class MyRolesComponent implements OnInit { |
||||
|
@Select(IdentityState.getRoles) |
||||
|
data$: Observable<IdentityRoleDto[]>; |
||||
|
|
||||
|
@Select(IdentityState.getRolesTotalCount) |
||||
|
totalCount$: Observable<number>; |
||||
|
|
||||
|
form: FormGroup; |
||||
|
|
||||
|
selected: IdentityRoleDto; |
||||
|
|
||||
|
isModalVisible: boolean; |
||||
|
|
||||
|
visiblePermissions = false; |
||||
|
|
||||
|
providerKey: string; |
||||
|
|
||||
|
modalBusy = false; |
||||
|
|
||||
|
permissionManagementKey = ePermissionManagementComponents.PermissionManagement; |
||||
|
|
||||
|
onVisiblePermissionChange = event => { |
||||
|
this.visiblePermissions = event; |
||||
|
}; |
||||
|
|
||||
|
constructor( |
||||
|
public readonly list: ListService<PagedAndSortedResultRequestDto>, |
||||
|
protected confirmationService: ConfirmationService, |
||||
|
protected store: Store, |
||||
|
protected injector: Injector |
||||
|
) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.hookToQuery(); |
||||
|
} |
||||
|
|
||||
|
buildForm() { |
||||
|
const data = new FormPropData(this.injector, this.selected); |
||||
|
this.form = generateFormFromProps(data); |
||||
|
} |
||||
|
|
||||
|
openModal() { |
||||
|
this.buildForm(); |
||||
|
this.isModalVisible = true; |
||||
|
} |
||||
|
|
||||
|
add() { |
||||
|
this.selected = {} as IdentityRoleDto; |
||||
|
this.openModal(); |
||||
|
} |
||||
|
|
||||
|
edit(id: string) { |
||||
|
this.store |
||||
|
.dispatch(new GetRoleById(id)) |
||||
|
.pipe(pluck('IdentityState', 'selectedRole')) |
||||
|
.subscribe(selectedRole => { |
||||
|
this.selected = selectedRole; |
||||
|
this.openModal(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
save() { |
||||
|
if (!this.form.valid) return; |
||||
|
this.modalBusy = true; |
||||
|
|
||||
|
this.store |
||||
|
.dispatch( |
||||
|
this.selected.id |
||||
|
? new UpdateRole({ ...this.selected, ...this.form.value, id: this.selected.id }) |
||||
|
: new CreateRole(this.form.value) |
||||
|
) |
||||
|
.pipe(finalize(() => (this.modalBusy = false))) |
||||
|
.subscribe(() => { |
||||
|
this.isModalVisible = false; |
||||
|
this.list.get(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
delete(id: string, name: string) { |
||||
|
this.confirmationService |
||||
|
.warn('AbpIdentity::RoleDeletionConfirmationMessage', 'AbpIdentity::AreYouSure', { |
||||
|
messageLocalizationParams: [name], |
||||
|
}) |
||||
|
.subscribe((status: Confirmation.Status) => { |
||||
|
if (status === Confirmation.Status.confirm) { |
||||
|
this.store.dispatch(new DeleteRole(id)).subscribe(() => this.list.get()); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private hookToQuery() { |
||||
|
this.list.hookToQuery(query => this.store.dispatch(new GetRoles(query))).subscribe(); |
||||
|
} |
||||
|
|
||||
|
openPermissionsModal(providerKey: string) { |
||||
|
this.providerKey = providerKey; |
||||
|
setTimeout(() => { |
||||
|
this.visiblePermissions = true; |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
sort(data) { |
||||
|
const { prop, dir } = data.sorts[0]; |
||||
|
this.list.sortKey = prop; |
||||
|
this.list.sortOrder = dir; |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
```js |
||||
|
{ |
||||
|
provide: EXTENSIONS_IDENTIFIER, |
||||
|
useValue: eIdentityComponents.Roles, |
||||
|
}, |
||||
|
{ |
||||
|
provide: RolesComponent, |
||||
|
useExisting: MyRolesComponent |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The two providers we have defined in `MyRolesComponent` are required for the extension components to work correctly. |
||||
|
|
||||
|
* With the first provider, we defined the extension identifier for using `RolesComponent`'s extension actions in the `MyRolesComponent`. |
||||
|
* With the second provider, we have replaced the `RolesComponent` injection with the `MyRolesComponent`. Default extension actions of the `RolesComponent` try to get `RolesComponent` instance. However, the actions can get the `MyRolesComponent` instance after defining the second provider. |
||||
|
|
||||
|
Open the generated `src/app/my-role/my-role.component.html` file and replace its content with the following: |
||||
|
|
||||
|
```html |
||||
|
<div id="identity-roles-wrapper" class="card"> |
||||
|
<div class="card-header"> |
||||
|
<div class="row"> |
||||
|
<div class="col col-md-6"> |
||||
|
<h5 class="card-title">My Roles</h5> |
||||
|
</div> |
||||
|
<div class="text-right col col-md-6"> |
||||
|
<abp-page-toolbar [record]="data$ | async"></abp-page-toolbar> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-body"> |
||||
|
<abp-extensible-table |
||||
|
[data]="data$ | async" |
||||
|
[recordsTotal]="totalCount$ | async" |
||||
|
[list]="list" |
||||
|
></abp-extensible-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<abp-modal size="md" [(visible)]="isModalVisible" [busy]="modalBusy"> |
||||
|
<ng-template #abpHeader> |
||||
|
<h3>{%{{{ (selected?.id ? 'AbpIdentity::Edit' : 'AbpIdentity::NewRole') | abpLocalization }}}%}</h3> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpBody> |
||||
|
<form [formGroup]="form" (ngSubmit)="save()" validateOnSubmit> |
||||
|
<abp-extensible-form [selectedRecord]="selected"></abp-extensible-form> |
||||
|
</form> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpFooter> |
||||
|
<button type="button" class="btn btn-secondary" #abpClose> |
||||
|
{%{{{ 'AbpIdentity::Cancel' | abpLocalization }}}%} |
||||
|
</button> |
||||
|
<abp-button iconClass="fa fa-check" [disabled]="form?.invalid" (click)="save()">{%{{{ |
||||
|
'AbpIdentity::Save' | abpLocalization |
||||
|
}}}%}</abp-button> |
||||
|
</ng-template> |
||||
|
</abp-modal> |
||||
|
|
||||
|
<abp-permission-management |
||||
|
#abpPermissionManagement="abpPermissionManagement" |
||||
|
*abpReplaceableTemplate=" |
||||
|
{ |
||||
|
inputs: { |
||||
|
providerName: { value: 'R' }, |
||||
|
providerKey: { value: providerKey }, |
||||
|
visible: { value: visiblePermissions, twoWay: true }, |
||||
|
hideBadges: { value: true } |
||||
|
}, |
||||
|
outputs: { visibleChange: onVisiblePermissionChange }, |
||||
|
componentKey: permissionManagementKey |
||||
|
}; |
||||
|
let init = initTemplate |
||||
|
" |
||||
|
(abpInit)="init(abpPermissionManagement)" |
||||
|
> |
||||
|
</abp-permission-management> |
||||
|
``` |
||||
|
|
||||
|
We have added the `abp-page-toolbar`, `abp-extensible-table`, and `abp-extensible-form` extension components to template of the `MyRolesComponent`. |
||||
|
|
||||
|
You should import the required modules for the `MyRolesComponent` to `MyRolesModule`. Open the `src/my-roles/my-roles.module.ts` file and replace the content with the following: |
||||
|
|
||||
|
```js |
||||
|
import { UiExtensionsModule } from '@abp/ng.theme.shared/extensions'; |
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { SharedModule } from '../shared/shared.module'; |
||||
|
import { MyRolesComponent } from './my-roles.component'; |
||||
|
import { PermissionManagementModule } from '@abp/ng.permission-management'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [MyRolesComponent], |
||||
|
imports: [SharedModule, UiExtensionsModule, PermissionManagementModule], |
||||
|
exports: [MyRolesComponent], |
||||
|
}) |
||||
|
export class MyRolesModule {} |
||||
|
``` |
||||
|
|
||||
|
- `UiExtensionsModule` imported to be able to use the extension components in your component. |
||||
|
- `PermissionManagementModule` imported to be able to use the `abp-permission-*management` in your component. |
||||
|
|
||||
|
As the last step, it is needs to be replaced the `RolesComponent` with the `MyRolesComponent`. Open the `app.component.ts` and modify its content as shown below: |
||||
|
|
||||
|
```js |
||||
|
import { ReplaceableComponentsService } from '@abp/ng.core'; |
||||
|
import { eIdentityComponents } from '@abp/ng.identity'; |
||||
|
import { MyRolesComponent } from './my-roles/my-roles.component'; |
||||
|
|
||||
|
@Component(/* component metadata */) |
||||
|
export class AppComponent { |
||||
|
constructor(private replaceableComponents: ReplaceableComponentsService) { |
||||
|
this.replaceableComponents.add({ component: MyRolesComponent, key: eIdentityComponents.Roles }); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
After the steps above, the `RolesComponent` has been successfully replaced with the `MyRolesComponent`. When you navigate to the `/identity/roles` URL, you will see the `MyRolesComponent`'s template and see the extension components working correctly. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
 |
||||
@ -0,0 +1,248 @@ |
|||||
|
# Modal |
||||
|
|
||||
|
`ModalComponent` is a pre-built component exposed by `@abp/ng.theme.shared` package to show modals. The component uses the [`ng-bootstrap`](https://ng-bootstrap.github.io/)'s modal service inside to render a modal. |
||||
|
|
||||
|
The `abp-modal` provides some additional benefits: |
||||
|
|
||||
|
- It is **flexible**. You can pass header, body, footer templates easily by adding the templates to the `abp-modal` content. It can also be implemented quickly. |
||||
|
- Provides several inputs be able to customize the modal and several outputs be able to listen to some events. |
||||
|
- Automatically detects the close button which has a `#abpClose` template variable and closes the modal when pressed this button. |
||||
|
- Automatically detects the `abp-button` and triggers its loading spinner when the `busy` input value of the modal component is true. |
||||
|
- Automatically checks if the form inside the modal **has changed, but not saved**. It warns the user by displaying a [confirmation popup](Confirmation-Service) in this case when a user tries to close the modal or refresh/close the tab of the browser. |
||||
|
|
||||
|
|
||||
|
> Note: A modal can also be rendered by using the `ng-bootstrap` modal. For further information, see [Modal doc](https://ng-bootstrap.github.io/#/components/modal) on the `ng-bootstrap` documentation. |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
In order to use the `abp-modal` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this: |
||||
|
|
||||
|
```js |
||||
|
// ... |
||||
|
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
//... |
||||
|
imports: [..., ThemeSharedModule], |
||||
|
}) |
||||
|
export class MyFeatureModule {} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can add the `abp-modal` to your component very quickly. See an example: |
||||
|
|
||||
|
```html |
||||
|
<!-- sample.component.html --> |
||||
|
|
||||
|
<button class="btn btn-primary" (click)="isModalOpen = true">Open modal</button> |
||||
|
|
||||
|
<abp-modal [(visible)]="isModalOpen"> |
||||
|
<ng-template #abpHeader> |
||||
|
<h3>Modal Title</h3> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpBody> |
||||
|
<p>Modal content</p> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpFooter> |
||||
|
<button type="button" class="btn btn-secondary" #abpClose>Close</button> |
||||
|
</ng-template> |
||||
|
</abp-modal> |
||||
|
``` |
||||
|
|
||||
|
```js |
||||
|
// sample.component.ts |
||||
|
|
||||
|
@Component(/* component metadata */) |
||||
|
export class SampleComponent { |
||||
|
isModelOpen = false |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
|
||||
|
See an example form inside a modal: |
||||
|
|
||||
|
```html |
||||
|
<!-- book.component.ts --> |
||||
|
|
||||
|
<abp-modal [(visible)]="isModalOpen" [busy]="inProgress"> |
||||
|
<ng-template #abpHeader> |
||||
|
<h3>Book</h3> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpBody> |
||||
|
<form id="book-form" [formGroup]="form" (ngSubmit)="save()"> |
||||
|
<div class="form-group"> |
||||
|
<label for="book-name">Author</label><span> * </span> |
||||
|
<input type="text" id="author" class="form-control" formControlName="author" autofocus /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="book-name">Name</label><span> * </span> |
||||
|
<input type="text" id="book-name" class="form-control" formControlName="name" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="book-price">Price</label><span> * </span> |
||||
|
<input type="number" id="book-price" class="form-control" formControlName="price" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="book-type">Type</label><span> * </span> |
||||
|
<select class="form-control" id="book-type" formControlName="type"> |
||||
|
<option [ngValue]="null">Select a book type</option> |
||||
|
<option [ngValue]="0">Undefined</option> |
||||
|
<option [ngValue]="1">Adventure</option> |
||||
|
<option [ngValue]="2">Biography</option> |
||||
|
<option [ngValue]="3">Fantastic</option> |
||||
|
<option [ngValue]="4">Science</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="book-publish-date">Publish date</label><span> * </span> |
||||
|
<input |
||||
|
id="book-publish-date" |
||||
|
formControlName="publishDate" |
||||
|
class="form-control" |
||||
|
type="date" |
||||
|
/> |
||||
|
</div> |
||||
|
</form> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpFooter> |
||||
|
<button type="button" class="btn btn-secondary" #abpClose> |
||||
|
Cancel |
||||
|
</button> |
||||
|
|
||||
|
<button form="book-form" class="btn btn-primary" [disabled]="form.invalid || form.pristine"> |
||||
|
<i class="fa fa-check mr-1"></i> |
||||
|
Save |
||||
|
</button> |
||||
|
</ng-template> |
||||
|
</abp-modal> |
||||
|
``` |
||||
|
|
||||
|
```ts |
||||
|
// book.component.ts |
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
import { FormBuilder, Validators } from '@angular/forms'; |
||||
|
|
||||
|
@Component(/* component metadata */) |
||||
|
export class BookComponent { |
||||
|
form = this.fb.group({ |
||||
|
author: [null, [Validators.required]], |
||||
|
name: [null, [Validators.required]], |
||||
|
price: [null, [Validators.required, Validators.min(0)]], |
||||
|
type: [null, [Validators.required]], |
||||
|
publishDate: [null, [Validators.required]], |
||||
|
}); |
||||
|
|
||||
|
inProgress: boolean; |
||||
|
|
||||
|
isModalOpen: boolean; |
||||
|
|
||||
|
constructor(private fb: FormBuilder, private service: BookService) {} |
||||
|
|
||||
|
save() { |
||||
|
if (this.form.invalid) return; |
||||
|
|
||||
|
this.inProgress = true; |
||||
|
|
||||
|
this.service.save(this.form.value).subscribe(() => { |
||||
|
this.inProgress = false; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The modal with form looks like this: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### Inputs |
||||
|
|
||||
|
#### visible |
||||
|
|
||||
|
```js |
||||
|
@Input() visible: boolean |
||||
|
``` |
||||
|
|
||||
|
**`visible`** is a boolean input that determines whether the modal is open. It is also can be used two-way binding. |
||||
|
|
||||
|
#### busy |
||||
|
|
||||
|
```js |
||||
|
@Input() busy: boolean |
||||
|
``` |
||||
|
|
||||
|
**`busy`** is a boolean input that determines whether the busy status of the modal is true. When `busy` is true, the modal cannot be closed and the `abp-button` loading spinner is triggered. |
||||
|
|
||||
|
|
||||
|
#### options |
||||
|
|
||||
|
```js |
||||
|
@Input() options: NgbModalOptions |
||||
|
``` |
||||
|
|
||||
|
**`options`** is an input typed [NgbModalOptions](https://ng-bootstrap.github.io/#/components/modal/api#NgbModalOptions). It is configuration for the `ng-bootstrap` modal. |
||||
|
|
||||
|
#### suppressUnsavedChangesWarning |
||||
|
|
||||
|
```js |
||||
|
@Input() suppressUnsavedChangesWarning: boolean |
||||
|
``` |
||||
|
|
||||
|
**`suppressUnsavedChangesWarning`** is a boolean input that determines whether the confirmation popup triggering active or not. It can also be set globally as shown below: |
||||
|
|
||||
|
```ts |
||||
|
//app.module.ts |
||||
|
|
||||
|
// app.module.ts |
||||
|
|
||||
|
import { SUPPRESS_UNSAVED_CHANGES_WARNING } from '@abp/ng.theme.shared'; |
||||
|
|
||||
|
// ... |
||||
|
|
||||
|
@NgModule({ |
||||
|
// ... |
||||
|
providers: [{provide: SUPPRESS_UNSAVED_CHANGES_WARNING, useValue: true}] |
||||
|
}) |
||||
|
export class AppModule {} |
||||
|
``` |
||||
|
|
||||
|
Note: The `suppressUnsavedChangesWarning` input of `abp-modal` value overrides the `SUPPRESS_UNSAVED_CHANGES_WARNING` injection token value. |
||||
|
|
||||
|
### Outputs |
||||
|
|
||||
|
#### visibleChange |
||||
|
|
||||
|
```js |
||||
|
@Output() readonly visibleChange = new EventEmitter<boolean>(); |
||||
|
``` |
||||
|
|
||||
|
**`visibleChange`** is an event emitted when the modal visibility has changed. The event payload is a boolean. |
||||
|
|
||||
|
#### appear |
||||
|
|
||||
|
```js |
||||
|
@Output() readonly appear = new EventEmitter<void>(); |
||||
|
``` |
||||
|
|
||||
|
**`appear`** is an event emitted when the modal has opened. |
||||
|
|
||||
|
#### disappear |
||||
|
|
||||
|
```js |
||||
|
@Output() readonly disappear = new EventEmitter<void>(); |
||||
|
``` |
||||
|
|
||||
|
**`disappear`** is an event emitted when the modal has closed. |
||||
@ -0,0 +1,146 @@ |
|||||
|
# Router Events Simplified |
||||
|
|
||||
|
`RouterEvents` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Benefit |
||||
|
|
||||
|
You can use router events directly and filter them as seen below: |
||||
|
|
||||
|
```js |
||||
|
import { |
||||
|
NavigationEnd, |
||||
|
NavigationError, |
||||
|
NavigationCancel, |
||||
|
Router, |
||||
|
} from '@angular/router'; |
||||
|
import { filter } from 'rxjs/operators'; |
||||
|
|
||||
|
@Injectable() |
||||
|
class SomeService { |
||||
|
navigationFinish$ = this.router.events.pipe( |
||||
|
filter( |
||||
|
event => |
||||
|
event instanceof NavigationEnd || |
||||
|
event instanceof NavigationError || |
||||
|
event instanceof NavigationCancel, |
||||
|
), |
||||
|
); |
||||
|
/* Observable<Event> */ |
||||
|
|
||||
|
constructor(private router: Router) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
However, `RouterEvents` makes filtering router events easier. |
||||
|
|
||||
|
```js |
||||
|
import { RouterEvents } from '@abp/ng.core'; |
||||
|
|
||||
|
@Injectable() |
||||
|
class SomeService { |
||||
|
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel'); |
||||
|
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */ |
||||
|
|
||||
|
constructor(private routerEvents: RouterEvents) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`RouterEvents` also delivers improved type-safety. In the example above, `navigationFinish$` has inferred type of `Observable<NavigationCancel | NavigationEnd | NavigationError>` whereas it would have `Observable<Event>` when router events are filtered directly. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You do not have to provide `RouterEvents` at the module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components. |
||||
|
|
||||
|
|
||||
|
### How to Get Specific Navigation Events |
||||
|
|
||||
|
You can use `getNavigationEvents` to get a stream of navigation events matching given event keys. |
||||
|
|
||||
|
```js |
||||
|
import { RouterEvents } from '@abp/ng.core'; |
||||
|
import { merge } from 'rxjs'; |
||||
|
import { mapTo } from 'rxjs/operators'; |
||||
|
|
||||
|
@Injectable() |
||||
|
class SomeService { |
||||
|
navigationStart$ = this.routerEvents.getNavigationEvents('Start'); |
||||
|
/* Observable<NavigationStart> */ |
||||
|
|
||||
|
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel'); |
||||
|
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */ |
||||
|
|
||||
|
loading$ = merge( |
||||
|
this.navigationStart$.pipe(mapTo(true)), |
||||
|
this.navigationFinish$.pipe(mapTo(false)), |
||||
|
); |
||||
|
/* Observable<boolean> */ |
||||
|
|
||||
|
constructor(private routerEvents: RouterEvents) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
### How to Get All Navigation Events |
||||
|
|
||||
|
You can use `getAllNavigationEvents` to get a stream of all navigation events without passing any keys. |
||||
|
|
||||
|
```js |
||||
|
import { RouterEvents, NavigationStart } from '@abp/ng.core'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
|
||||
|
@Injectable() |
||||
|
class SomeService { |
||||
|
navigationEvent$ = this.routerEvents.getAllNavigationEvents(); |
||||
|
/* Observable<NavigationCancel | NavigationEnd | NavigationError | NavigationStart> */ |
||||
|
|
||||
|
loading$ = this.navigationEvent$.pipe( |
||||
|
map(event => event instanceof NavigationStart), |
||||
|
); |
||||
|
/* Observable<boolean> */ |
||||
|
|
||||
|
constructor(private routerEvents: RouterEvents) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
### How to Get Specific Router Events |
||||
|
|
||||
|
You can use `getEvents` to get a stream of router events matching given event constructors. |
||||
|
|
||||
|
```js |
||||
|
import { RouterEvents } from '@abp/ng.core'; |
||||
|
import { ActivationEnd, ChildActivationEnd } from '@angular/router'; |
||||
|
|
||||
|
@Injectable() |
||||
|
class SomeService { |
||||
|
moduleActivation$ = this.routerEvents.getEvents(ActivationEnd, ChildActivationEnd); |
||||
|
/* Observable<ActivationEnd | ChildActivationEnd> */ |
||||
|
|
||||
|
constructor(private routerEvents: RouterEvents) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
### How to Get All Router Events |
||||
|
|
||||
|
You can use `getEvents` to get a stream of all router events without passing any event constructors. This is nothing different from accessing `events` property of `Router` and is added to the service just for convenience. |
||||
|
|
||||
|
```js |
||||
|
import { RouterEvents } from '@abp/ng.core'; |
||||
|
import { ActivationEnd, ChildActivationEnd } from '@angular/router'; |
||||
|
|
||||
|
@Injectable() |
||||
|
class SomeService { |
||||
|
routerEvent$ = this.routerEvents.getAllEvents(); |
||||
|
/* Observable<Event> */ |
||||
|
|
||||
|
constructor(private routerEvents: RouterEvents) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 108 KiB |
@ -1,3 +1,153 @@ |
|||||
# RabbitMQ Background Job Manager |
# RabbitMQ 后台作业管理 |
||||
|
|
||||
待添加 |
RabbitMQ 是一个标准的消息队列中间件,虽然它常用于消息传递/分布式事件,但也非常适合存储 FIFO(先进先出) 顺序的后台作业. |
||||
|
|
||||
|
ABP Framework 提供了 [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) 包,将使用 RabbitMQ 来执行后台作业. |
||||
|
|
||||
|
> 参阅 [后台作业文档](Background-Jobs.md) 学习如何使用后台作业系统,本文只介绍了如何安装和配置 RabbitMQ 集成. |
||||
|
|
||||
|
## 安装 |
||||
|
|
||||
|
使用 ABP CLI 将 [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) 包添加到你的项目: |
||||
|
|
||||
|
- 如果之前没有安装过 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI),请先安装它. |
||||
|
- 跳转到待安装后台作业管理的项目目录中(包含 `.csproj` 文件的目录),打开终端管理器. |
||||
|
- 执行 `abp add-package Volo.Abp.BackgroundJobs.RabbitMQ` 命令. |
||||
|
|
||||
|
如果你想要手动安装,请先用 NuGet 包管理器安装 [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) 包到指定项目,之后使在你的 [模块](Module-Development-Basics.md) 上面添加 `[DependsOn(typeof(AbpBackgroundJobsRabbitMqModule))]` 配置依赖. |
||||
|
|
||||
|
## 配置 |
||||
|
|
||||
|
### 默认配置 |
||||
|
|
||||
|
默认配置将会使用标准端口和主机名(localhost)连接到 RabbitMQ 服务,**你不需要进行额外配置**. |
||||
|
|
||||
|
### RabbitMQ 连接 |
||||
|
|
||||
|
你可以使用 ASP.NET Core 的 [标准配置系统](Configuration.md) 对 RabbitMQ 进行详细配置,比如 `appsettings.json` 或者是 [选项类](Options.md). |
||||
|
|
||||
|
#### 通过 `appsettings.json` 文件配置 |
||||
|
|
||||
|
这种方式是配置 RabbitMQ 连接最简单的方式,你可以使用其他的配置源(例如环境变量).这些强大的功能都是由 [ASP.NET Core](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/) 提供的支持. |
||||
|
|
||||
|
**示例: 配置默认的 RabbitMQ 连接** |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"RabbitMQ": { |
||||
|
"Connections": { |
||||
|
"Default": { |
||||
|
"HostName": "123.123.123.123", |
||||
|
"Port": "5672" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
你可以在配置文件使用所有 [ConnectionFactry](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties) 的属性,关于这些属性的具体含义,可以查看 RabbitMQ 的 [官方文档](https://www.rabbitmq.com/dotnet-api-guide.html#exchanges-and-queues). |
||||
|
|
||||
|
目前我们允许定义多个连接,多连接的情况适用于不同的后台作业,具体配置信息可以参考下面的 RabbitMQ 后台作业配置说明. |
||||
|
|
||||
|
**示例: 定义两个 RabbitMQ 连接** |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"RabbitMQ": { |
||||
|
"Connections": { |
||||
|
"Default": { |
||||
|
"HostName": "123.123.123.123" |
||||
|
}, |
||||
|
"SecondConnection": { |
||||
|
"HostName": "321.321.321.321" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
如果需要连接到 RabbitMQ 集群,你可以指定多个 HostName. |
||||
|
|
||||
|
**示例: 连接到 RabbitMQ 集群** |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"RabbitMQ": { |
||||
|
"Connections": { |
||||
|
"Default": { |
||||
|
"HostName": "123.123.123.123;234.234.234.234" |
||||
|
} |
||||
|
}, |
||||
|
"EventBus": { |
||||
|
"ClientName": "MyClientName", |
||||
|
"ExchangeName": "MyExchangeName" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 使用选项类 |
||||
|
|
||||
|
`AbpRabbitMqOptions` 类型用于配置 RabbitMQ 的连接字符串,你可以在 [模块](Module-Development-Basics.md) 的 `ConfigureService` 方法中进行配置. |
||||
|
|
||||
|
**示例: 配置 RabbitMQ 连接** |
||||
|
|
||||
|
```csharp |
||||
|
Configure<AbpRabbitMqOptions>(options => |
||||
|
{ |
||||
|
options.Connections.Default.UserName = "user"; |
||||
|
options.Connections.Default.Password = "pass"; |
||||
|
options.Connections.Default.HostName = "123.123.123.123"; |
||||
|
options.Connections.Default.Port = 5672; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
关于选项类,可以结合 `appsettings.json` 文件一起使用.针对同一个属性,在选项类里面对该值进行了设定,会覆盖掉 `appsettings.json` 的值. |
||||
|
|
||||
|
### RabbitMQ 后台作业配置说明 |
||||
|
|
||||
|
#### 后台作业队列的名称 |
||||
|
|
||||
|
默认情况下,每个后台作业都会使用一个单独的队列,结合标准前缀和作业名称来构造一个完整的队列名称.默认的前缀为 `AbpBackgroundJobs`,所以有一个作业的名称是 `EmailSending` 的话,在 RabbitMQ 的队列名称就是 `AbpBackgroundJobs.EmailSending`. |
||||
|
|
||||
|
> 在后台作业的参数类上,可以使用 `BackgroundJobName` 特性指定后台作业的名称.否则的话,后台作业的名称将会是后台作业类的全名(也包含命名空间). |
||||
|
|
||||
|
#### 后台作业使用的连接 |
||||
|
|
||||
|
默认情况下,后台作业都会使用 `Default` 作为默认连接. |
||||
|
|
||||
|
#### 自定义 |
||||
|
|
||||
|
`AbpRabbitMqBackgroundJobOptions` 可以自定义队列名和作业使用的 RabbitMQ 连接. |
||||
|
|
||||
|
**示例: ** |
||||
|
|
||||
|
```csharp |
||||
|
Configure<AbpRabbitMqBackgroundJobOptions>(options => |
||||
|
{ |
||||
|
options.DefaultQueueNamePrefix = "my_app_jobs."; |
||||
|
options.JobQueues[typeof(EmailSendingArgs)] = |
||||
|
new JobQueueConfiguration( |
||||
|
typeof(EmailSendingArgs), |
||||
|
queueName: "my_app_jobs.emails", |
||||
|
connectionName: "SecondConnection" |
||||
|
); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
- 这个示例将默认的队列名前缀设置为 `my_app_jobs.`,如果多个项目都使用的同一个 RabbitMQ 服务,设置不同的前缀可以避免执行其他项目的后台作业. |
||||
|
- 这里还设置了 `EmailSendingArgs` 绑定的 RabbitMQ 连接. |
||||
|
|
||||
|
`JobQueueConfiguration` 类的构造函数中,还有一些其他的可选参数. |
||||
|
|
||||
|
- `queueName`: 指定后台作业对应的队列名称(全名). |
||||
|
- `connectionName`: 后台作业对应的 RabbitMQ 连接名称,默认是 `Default`. |
||||
|
- `durable`: 可选参数,默认为 `true`. |
||||
|
- `exclusive`: 可选参数,默认为 `false`. |
||||
|
- `autoDelete`: 可选参数,默认为 `false`. |
||||
|
|
||||
|
如果你想要更多地了解 `durable`,`exclusive`,`autoDelete` 的用法,请阅读 RabbitMQ 提供的文档. |
||||
|
|
||||
|
## 另请参阅 |
||||
|
|
||||
|
- [后台作业](Background-Jobs.md) |
||||
|
|||||
@ -0,0 +1,233 @@ |
|||||
|
# 模块化插件 |
||||
|
|
||||
|
可以将[模块](Module-Development-Basics.md)加载为插件.这意味着你可能不需要在解决方案中引用模块的程序集,就可以像其它模块一样在启动应用时加载该模块. |
||||
|
|
||||
|
## 基本用法 |
||||
|
|
||||
|
`IServiceCollection.AddApplication<T>()` 扩展方法可以获取配置插件源的选项. |
||||
|
|
||||
|
**示例: 从文件夹加载插件** |
||||
|
|
||||
|
````csharp |
||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.Modularity.PlugIns; |
||||
|
|
||||
|
namespace MyPlugInDemo.Web |
||||
|
{ |
||||
|
public class Startup |
||||
|
{ |
||||
|
public void ConfigureServices(IServiceCollection services) |
||||
|
{ |
||||
|
services.AddApplication<MyPlugInDemoWebModule>(options => |
||||
|
{ |
||||
|
options.PlugInSources.AddFolder(@"D:\Temp\MyPlugIns"); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void Configure(IApplicationBuilder app) |
||||
|
{ |
||||
|
app.InitializeApplication(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 这是典型的ASP.NET Core应用程序的`Startup`类. |
||||
|
* `PlugInSources.AddFolder`从指定的目录中加载程序集(通常为dll). |
||||
|
|
||||
|
就这样.ABP将在这个目录中发现这些模块,像其它常规一样配置和初始化它们. |
||||
|
|
||||
|
### 插件源 |
||||
|
|
||||
|
`options.PlugInSources`类实际上是`IPlugInSource`接口的一系列实现并且 `AddFolder`方法仅仅是以下表达式的便捷方法: |
||||
|
|
||||
|
````csharp |
||||
|
options.PlugInSources.Add(new FolderPlugInSource(@"D:\Temp\MyPlugIns")); |
||||
|
```` |
||||
|
|
||||
|
> `AddFolder()`方法仅在给定目录下查找程序集文件,而不在子目录中查找.你可以传递一个`SearchOption.AllDirectories`参数作为第二个参数,来递归地查找它的子目录. |
||||
|
|
||||
|
这里有两个内置插件源的示例: |
||||
|
|
||||
|
* `PlugInSources.AddFiles()`方法获取程序集(通常是dll)文件列表.这是使用`FilePlugInSource`类的快捷方式. |
||||
|
* `PlugInSources.AddTypes()`方法获取模块类类型的列表.如果实用化此方法,则需要自己加载模块的程序集,但是在需要时它提供了灵活性.这是使用`TypePlugInSource`类的快捷方式. |
||||
|
|
||||
|
如果需要,你可以创建自己的`IPlugInSource`的接口实现,并像其它方法一样添加到`options.PlugInSources`中. |
||||
|
|
||||
|
## 示例:创建一个简单的插件 |
||||
|
|
||||
|
在一个解决方案中创建一个简单的**类库项目** |
||||
|
|
||||
|
 |
||||
|
|
||||
|
你可以在模块中添加需要使用的ABP框架包.至少,你应该为这个项目添加包`Volo.Abp.Core`: |
||||
|
|
||||
|
```` |
||||
|
Install-Package Volo.Abp.Core |
||||
|
```` |
||||
|
|
||||
|
每个[模块](Module-Development-Basics.md)必须声明为一个继承自`AbpModule`的类.这里是一个简单的模块类,用于解析一个服务并在应用启动时对其初始化: |
||||
|
|
||||
|
````csharp |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace MyPlugIn |
||||
|
{ |
||||
|
public class MyPlungInModule : AbpModule |
||||
|
{ |
||||
|
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
||||
|
{ |
||||
|
var myService = context.ServiceProvider |
||||
|
.GetRequiredService<MyService>(); |
||||
|
|
||||
|
myService.Initialize(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
`MyService`可以是注册在[依赖注入](Dependency-Injection.md)系统中的任意类,如下所示: |
||||
|
|
||||
|
````csharp |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace MyPlugIn |
||||
|
{ |
||||
|
public class MyService : ITransientDependency |
||||
|
{ |
||||
|
private readonly ILogger<MyService> _logger; |
||||
|
|
||||
|
public MyService(ILogger<MyService> logger) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public void Initialize() |
||||
|
{ |
||||
|
_logger.LogInformation("MyService has been initialized"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
编译这个项目,打开build目录,找到`MyPlugIn.dll`: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
将`MyPlugIn.dll`复制到到插件目录中(此实例为`D:\Temp\MyPlugIns`). |
||||
|
|
||||
|
如果你已经按照上述方式配置了主应用程序(参见“基础用法”部分),那么在应用程序启动时,你可以看到“MyService has been initialized(MyService已经初始化)的日志. |
||||
|
|
||||
|
## 示例:创建一个Razor Pages插件 |
||||
|
|
||||
|
创建内部带视图的插件需要更多的注意. |
||||
|
|
||||
|
> 这个示例假设你已经使用应用程序启动模板和MVC / Razor Pages UI[创建了一个新的Web应用程序](https://abp.io/get-started). |
||||
|
|
||||
|
在解决方案中创建一个新的**类库**项目: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
编辑这个`.csproj`文件内容: |
||||
|
|
||||
|
````xml |
||||
|
<Project Sdk="Microsoft.NET.Sdk.Web"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net5.0</TargetFramework> |
||||
|
<OutputType>Library</OutputType> |
||||
|
<IsPackable>true</IsPackable> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared" Version="4.0.1" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
|
```` |
||||
|
|
||||
|
* 将`Sdk`修改为`Microsoft.NET.Sdk.Web`. |
||||
|
* 添加了`OutputType`和`IsPackable`属性. |
||||
|
* 添加了`Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared`NuGet包. |
||||
|
|
||||
|
> 不需要[Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared) 包.你可以引用更基础的程序包,例如[Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc/). 但是,如果需要构建一个UI视图/组件,建议参考[Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared)程序包,因为它是最高级的程序包,不依赖于特定[theme](UI/AspNetCore/Theming.md).如果依赖特定主题没有问题,则可以直接引用该主题的程序包,以便能够使用插件中特定于主题的功能. |
||||
|
|
||||
|
接下来在插件中创建模块类: |
||||
|
|
||||
|
````csharp |
||||
|
using System.IO; |
||||
|
using System.Reflection; |
||||
|
using Microsoft.AspNetCore.Mvc.ApplicationParts; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace MyMvcUIPlugIn |
||||
|
{ |
||||
|
[DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))] |
||||
|
public class MyMvcUIPlugInModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
PreConfigure<IMvcBuilder>(mvcBuilder => |
||||
|
{ |
||||
|
// 添加插件程序集 |
||||
|
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(MyMvcUIPlugInModule).Assembly)); |
||||
|
|
||||
|
// 添加视图程序集 |
||||
|
var viewDllPath = Path.Combine(Path.GetDirectoryName(typeof(MyMvcUIPlugInModule).Assembly.Location), "MyMvcUIPlugIn.Views.dll"); |
||||
|
var viewAssembly = new CompiledRazorAssemblyPart(Assembly.LoadFrom(viewDllPath)); |
||||
|
mvcBuilder.PartManager.ApplicationParts.Add(viewAssembly); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 由于我们添加了相关的NuGet包,因此取决于`AbpAspNetCoreMvcUiThemeSharedModule`. |
||||
|
* 添加插件程序集到ASP.NET Core MVC的`PartManager`中.这是ASP.NET Core所必需的.否则,你插件中的控制器将无法正常工作. |
||||
|
* 添加插件的视图程序集到ASP.NET Core MVC的`PartManager`中.这是ASP.NET Core所必需的.否则,你在插件中的视图将不起作用. |
||||
|
|
||||
|
现在,你可以在`Pages`目录下添加一个razor页面,例如`MyPlugInPage.cshtml`: |
||||
|
|
||||
|
````html |
||||
|
@page |
||||
|
@model MyMvcUIPlugIn.Pages.MyPlugInPage |
||||
|
<h1>Welcome to my plug-in page</h1> |
||||
|
<p>This page is located inside a plug-in module! :)</p> |
||||
|
```` |
||||
|
|
||||
|
现在,你可以构建插件项目.它将产生以下输出: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
将`MyMvcUIPlugIn.dll`和`MyMvcUIPlugIn.Views.dll`复制到到插件目录下(此示例中为`D:\Temp\MyPlugIns`). |
||||
|
|
||||
|
如果你已经按照上述方式配置了主应用程序(参见“基础用法”部分),那么在应用程序启动的时候,你应该能够访问`/MyPlugInPage`URL: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 讨论 |
||||
|
|
||||
|
在现实世界中,你的插件可能具有一些外部依赖性.另外,你的应用程序可能被设计为支持插件.所有这些都是你自己的系统要求.ABP做的仅仅是在应用程序启动时加载模块.你在这些模块中执行什么操作由你决定. |
||||
|
|
||||
|
但是,我们可以为一些常见情况提供一些建议. |
||||
|
|
||||
|
### 库依赖 |
||||
|
|
||||
|
对于包/dll依赖,你可以将相关的dll复制到插件目录下.ABP会自动将所有程序集加载到该目录下,并且你的插件将按预期工作. |
||||
|
|
||||
|
> 请参见[Microsoft文档](https://docs.microsoft.com/zh-cn/dotnet/core/tutorials/creating-app-with-plugin-support#plugin-with-library-dependencies). |
||||
|
|
||||
|
### 数据库模式 |
||||
|
|
||||
|
如果你的模块使用关系型数据库和[Entity Framework Core](Entity-Framework-Core.md), 那么它需要在数据库中提供表.有多种不同的方法可确保在应用程序使用插件时创建表.一些例子; |
||||
|
|
||||
|
1. 插件可以检查数据库表是否存在,并在应用程序启动时创建表,或者如果插件已更新且需要进行某些架构更改时,则会迁移它们.你可以使用EF Core的迁移API来做到这一点. |
||||
|
2. 你可以改进`DbMigrator`应用程序,用于查找插件的迁移并执行它们. |
||||
|
|
||||
|
可能还有其它解决方案.例如,如果你的数据库管理员不允许你在应用程序代码中更改数据库模式,则可能需要手动将SQL文件发送给数据库管理员,以将其应用于数据库. |
||||
@ -0,0 +1 @@ |
|||||
|
TODO... |
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 25 KiB |