@ -0,0 +1 @@ |
|||||
|
Please describe the feature need! |
||||
@ -0,0 +1,71 @@ |
|||||
|
# For most projects, this workflow file will not need changing; you simply need |
||||
|
# to commit it to your repository. |
||||
|
# |
||||
|
# You may wish to alter this file to override the set of languages analyzed, |
||||
|
# or to provide custom queries or build logic. |
||||
|
name: "CodeQL" |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
branches: [dev, rel-1.0, rel-1.1, rel-2.0, rel-2.1, rel-2.2, rel-2.3, rel-2.4, rel-2.5, rel-2.6, rel-2.7, rel-2.8, rel-2.9, rel-3.0, rel-3.1, rel-3.2] |
||||
|
pull_request: |
||||
|
# The branches below must be a subset of the branches above |
||||
|
branches: [dev] |
||||
|
schedule: |
||||
|
- cron: '0 8 * * 6' |
||||
|
|
||||
|
jobs: |
||||
|
analyze: |
||||
|
name: Analyze |
||||
|
runs-on: ubuntu-latest |
||||
|
|
||||
|
strategy: |
||||
|
fail-fast: false |
||||
|
matrix: |
||||
|
# Override automatic language detection by changing the below list |
||||
|
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] |
||||
|
language: ['csharp', 'javascript'] |
||||
|
# Learn more... |
||||
|
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout repository |
||||
|
uses: actions/checkout@v2 |
||||
|
with: |
||||
|
# We must fetch at least the immediate parents so that if this is |
||||
|
# a pull request then we can checkout the head. |
||||
|
fetch-depth: 2 |
||||
|
|
||||
|
# If this run was triggered by a pull request event, then checkout |
||||
|
# the head of the pull request instead of the merge commit. |
||||
|
- run: git checkout HEAD^2 |
||||
|
if: ${{ github.event_name == 'pull_request' }} |
||||
|
|
||||
|
# Initializes the CodeQL tools for scanning. |
||||
|
- name: Initialize CodeQL |
||||
|
uses: github/codeql-action/init@v1 |
||||
|
with: |
||||
|
languages: ${{ matrix.language }} |
||||
|
# If you wish to specify custom queries, you can do so here or in a config file. |
||||
|
# By default, queries listed here will override any specified in a config file. |
||||
|
# Prefix the list here with "+" to use these queries and those in the config file. |
||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main |
||||
|
|
||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). |
||||
|
# If this step fails, then you should remove it and run the build manually (see below) |
||||
|
- name: Autobuild |
||||
|
uses: github/codeql-action/autobuild@v1 |
||||
|
|
||||
|
# ℹ️ Command-line programs to run using the OS shell. |
||||
|
# 📚 https://git.io/JvXDl |
||||
|
|
||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines |
||||
|
# and modify them (or add more) to build your code if your project |
||||
|
# uses a compiled language |
||||
|
|
||||
|
#- run: | |
||||
|
# make bootstrap |
||||
|
# make release |
||||
|
|
||||
|
- name: Perform CodeQL Analysis |
||||
|
uses: github/codeql-action/analyze@v1 |
||||
@ -0,0 +1,47 @@ |
|||||
|
# ABP Framework 3.2 Final Has Been Released |
||||
|
|
||||
|
ABP Framework & ABP Commercial 3.2 have been released today. |
||||
|
|
||||
|
Since all the new features are already explained in details with the [3.2 RC Announcement Post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI), I will not repeat all the details again. Please read [the RC post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI) for **new feature and changes** you may need to do for your solution while upgrading to the version 3.2. |
||||
|
|
||||
|
## Creating New Solutions |
||||
|
|
||||
|
You can create a new solution with the ABP Framework version 3.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 |
||||
|
```` |
||||
|
|
||||
|
After the update command, check [the RC blog post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI) to learn if you need to make any changes in your solution. |
||||
|
|
||||
|
> You may want to see the new [upgrading document](https://docs.abp.io/en/abp/latest/Upgrading). |
||||
|
|
||||
|
## About the Next Versions |
||||
|
|
||||
|
The next two versions (3.3 & 4.0) will be mostly related to completing the Blazor UI features and upgrading the ABP Framework & ecosystem to the .NET 5.0. |
||||
|
|
||||
|
The ultimate goal is to complete the version 4.0 with a stable Blazor UI with the fundamental features implemented and publish it just after the Microsoft lunches .NET 5 in this November. |
||||
@ -0,0 +1,317 @@ |
|||||
|
# Using DevExtreme Angular Components With the ABP Framework |
||||
|
|
||||
|
In this article, I will show you how to integrate the [DevExpress Angular components](https://js.devexpress.com/Documentation/Guide/Angular_Components/DevExtreme_Angular_Components/) to a project created using the ABP Framework startup templates. Then I will use the [DataGrid](https://js.devexpress.com/Documentation/Guide/Widgets/DataGrid/Overview/) component to show a list of users on the UI. |
||||
|
|
||||
|
## Create the Project |
||||
|
|
||||
|
Let's create a new web application with the Angular UI using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI#new): |
||||
|
|
||||
|
```shell |
||||
|
abp new DevExtremeAngular -u angular |
||||
|
``` |
||||
|
|
||||
|
> For detail information about how to generate and start up a project, please refer to the [official docs](https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&DB=EF&Tiered=No). For the scope of this post, we will not go into details of the backend applications. |
||||
|
|
||||
|
## Running the Solution |
||||
|
|
||||
|
### The Server Side |
||||
|
|
||||
|
Server side contains multiple projects in the solution: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Run following projects in order; |
||||
|
|
||||
|
* Run `DevExtremeAngular.DbMigrator` to create the database and seed the initial data. |
||||
|
* Run `DevExtremeAngular.HttpApi.Host` project to make the backend API up & running. |
||||
|
|
||||
|
### The Angular Application |
||||
|
|
||||
|
Open a command line terminal and navigate to `angular` folder then run `yarn` or `npm install` based on which package you are using. |
||||
|
|
||||
|
After installation process is done, you can start your angular project by running `yarn start` or `npm start`. This command should serve the application and open the application in your default browser. If it doesn't open, you can navigate to http://localhost:4200 in your browser: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
You can login to the application by using following credentials: |
||||
|
|
||||
|
> _Default admin username is **admin** and password is **1q2w3E\***_ |
||||
|
|
||||
|
 |
||||
|
|
||||
|
After successful login, you should be redirected to home page. |
||||
|
|
||||
|
## Install DevExtreme |
||||
|
|
||||
|
You can follow [the guide](https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Add_DevExtreme_to_an_Angular_CLI_Application/) provided by **DevExtreme** team or apply the following steps. |
||||
|
|
||||
|
* `npm install devextreme devextreme-angular` or `yarn add devextreme devextreme-angular` |
||||
|
* Import given two following styles in `angular.json` file: |
||||
|
|
||||
|
```javascript |
||||
|
// ... |
||||
|
"styles": [ |
||||
|
// ... |
||||
|
"src/styles.scss", |
||||
|
|
||||
|
"node_modules/devextreme/dist/css/dx.common.css", |
||||
|
"node_modules/devextreme/dist/css/dx.light.css" |
||||
|
] |
||||
|
``` |
||||
|
|
||||
|
* Add `dx-viewport` to classes of `body` in `index.html` |
||||
|
|
||||
|
```html |
||||
|
<body class="bg-light dx-viewport"> |
||||
|
<app-root> |
||||
|
<div class="donut centered"></div> |
||||
|
</app-root> |
||||
|
</body> |
||||
|
``` |
||||
|
|
||||
|
After completing these steps, you need to restart the angular application. |
||||
|
|
||||
|
## Create a lazy Angular Module for DevExtreme Demo |
||||
|
|
||||
|
Let's create a module which will be loaded lazily. |
||||
|
|
||||
|
Open up a terminal and navigate to `angular` to run following command. |
||||
|
|
||||
|
```shell |
||||
|
ng g m dev-extreme --route dev-extreme --module app |
||||
|
``` |
||||
|
|
||||
|
...or with `npx`, if you do not have `angular-cli` installed... |
||||
|
|
||||
|
```shell |
||||
|
npx ng g m dev-extreme --route dev-extreme --module app |
||||
|
``` |
||||
|
|
||||
|
Your terminal should log the following output: |
||||
|
|
||||
|
```shell |
||||
|
CREATE src/app/dev-extreme/dev-extreme-routing.module.ts (361 bytes) |
||||
|
CREATE src/app/dev-extreme/dev-extreme.module.ts (379 bytes) |
||||
|
CREATE src/app/dev-extreme/dev-extreme.component.scss (0 bytes) |
||||
|
CREATE src/app/dev-extreme/dev-extreme.component.html (26 bytes) |
||||
|
CREATE src/app/dev-extreme/dev-extreme.component.spec.ts (655 bytes) |
||||
|
CREATE src/app/dev-extreme/dev-extreme.component.ts (295 bytes) |
||||
|
UPDATE src/app/app-routing.module.ts (362 bytes) |
||||
|
``` |
||||
|
|
||||
|
The Angular CLI has created a module and configured it to lazy-load at `/dev-extreme` path. |
||||
|
|
||||
|
The last step to be able to see our newly created module in the browser, open `route.provider.ts` and edit the array being added into the routes. |
||||
|
|
||||
|
```typescript |
||||
|
// ... |
||||
|
routes.add([ |
||||
|
{ |
||||
|
path: '/', |
||||
|
name: '::Menu:Home', |
||||
|
iconClass: 'fas fa-home', |
||||
|
order: 1, |
||||
|
layout: eLayoutType.application, |
||||
|
}, |
||||
|
{ |
||||
|
path: '/dev-extreme', |
||||
|
name: 'Dev Extreme', |
||||
|
order: 2, |
||||
|
layout: eLayoutType.application, |
||||
|
}, |
||||
|
]); |
||||
|
// ... |
||||
|
``` |
||||
|
|
||||
|
After completing the steps above, you should be able to see `Dev Extreme` on the header and when you click on it, you should be redirected to `/dev-extreme` page and see the following message on the screen. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Display users on the dev-extreme page |
||||
|
|
||||
|
For this demo, we will list users on the screen. We already have `admin` as our first user. |
||||
|
|
||||
|
Let's add couple of more to the list in `Administration -> Identity Management -> Users` page. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Now we are ready to fetch our users and display them on `/dev-extreme` page. |
||||
|
|
||||
|
Firstly, let's create a service for our component. |
||||
|
|
||||
|
Navigate to the `dev-extreme` folder and run following command. If you run this command at the root, the service will be generated next to `app.module.ts` |
||||
|
|
||||
|
```shell |
||||
|
ng g s dev-extreme |
||||
|
``` |
||||
|
|
||||
|
Following files should be created |
||||
|
|
||||
|
```shell |
||||
|
CREATE src/app/dev-extreme/dev-extreme.service.spec.ts (378 bytes) |
||||
|
CREATE src/app/dev-extreme/dev-extreme.service.ts (139 bytes) |
||||
|
``` |
||||
|
|
||||
|
Let's import and inject `IdentityService` as dependency in `dev-extreme.service.ts`. After then, let's create a stream called `users$` to retrieve the users. |
||||
|
|
||||
|
`identityService.getUsers` returns `ABP.PagedResponse` which contains two fields, `items` and `totalCount`. We are only interested in `items` for now. |
||||
|
|
||||
|
When we apply the steps described above, the final version of `dev-extreme.service` should be as follows |
||||
|
|
||||
|
```typescript |
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
import { IdentityService } from '@abp/ng.identity'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root', |
||||
|
}) |
||||
|
export class DevExtremeService { |
||||
|
users$ = this.service.getUsers().pipe(map((result) => result.items)); |
||||
|
|
||||
|
constructor(private service: IdentityService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Now we can simply inject `DevExtremeService` as public and utilize `users$` stream in `dev-extreme.component.ts` as follows: |
||||
|
|
||||
|
```typescript |
||||
|
import { Component } from '@angular/core'; |
||||
|
import { DevExtremeService } from './dev-extreme.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-dev-extreme', |
||||
|
templateUrl: './dev-extreme.component.html', |
||||
|
styleUrls: ['./dev-extreme.component.scss'], |
||||
|
}) |
||||
|
export class DevExtremeComponent { |
||||
|
constructor(public service: DevExtremeService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
And use it within `dev-extreme.component.html` |
||||
|
|
||||
|
```html |
||||
|
<ng-container *ngIf="service.users$ | async as users"> |
||||
|
<ul> |
||||
|
<li *ngFor="let user of users"> |
||||
|
{{ user.name }} |
||||
|
</li> |
||||
|
</ul> |
||||
|
</ng-container> |
||||
|
``` |
||||
|
|
||||
|
This should list names of the users on the screen |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Use DxDataGrid to list the users |
||||
|
|
||||
|
You can take a look at [demo](https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/ColumnCustomization/Angular/Light/) provided by **DevExtreme** team or apply the following steps. |
||||
|
|
||||
|
Now, our application is ready to use `dx-data-grid` in `dev-extreme.component.ts` |
||||
|
|
||||
|
Firstly, we need to import `DxDataGridModule` in our module as follows. |
||||
|
|
||||
|
```typescript |
||||
|
// ... |
||||
|
|
||||
|
import { DxDataGridModule } from 'devextreme-angular'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
// ... |
||||
|
imports: [ |
||||
|
// ... |
||||
|
DxDataGridModule |
||||
|
], |
||||
|
}) |
||||
|
export class DevExtremeModule {} |
||||
|
``` |
||||
|
|
||||
|
At this point `dx-data-grid` is avaliable within our module and we can use it in our template. |
||||
|
|
||||
|
Change `dev-extreme.component.html` to the following |
||||
|
|
||||
|
```html |
||||
|
<ng-container *ngIf="service.users$ | async as users"> |
||||
|
<dx-data-grid [dataSource]="users"></dx-data-grid> |
||||
|
</ng-container> |
||||
|
``` |
||||
|
|
||||
|
It should display a table on the screen |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Since, we did not specify any columns, `dx-data-grid` displayed every column avaliable. Let's pick some columns to make it more readable. |
||||
|
|
||||
|
Change `dev-extreme.component.html` to the following: |
||||
|
|
||||
|
```html |
||||
|
<ng-container *ngIf="service.users$ | async as users"> |
||||
|
<dx-data-grid [dataSource]="users"> |
||||
|
<dxi-column dataField="userName"></dxi-column> |
||||
|
<dxi-column dataField="name"></dxi-column> |
||||
|
<dxi-column dataField="surname"></dxi-column> |
||||
|
<dxi-column dataField="email"></dxi-column> |
||||
|
<dxi-column dataField="phoneNumber"></dxi-column> |
||||
|
</dx-data-grid> |
||||
|
</ng-container> |
||||
|
``` |
||||
|
|
||||
|
which will display following table on the screen |
||||
|
|
||||
|
 |
||||
|
|
||||
|
We can also utilize `abpLocalization` pipe to translate the headers of the table. To use `abpLocalization` pipe in our templates, we need to import `CoreModule` from `@abp/ng.core` into our module. |
||||
|
|
||||
|
```typescript |
||||
|
import { CoreModule } from '@abp/ng.core'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
// ... |
||||
|
imports: [ |
||||
|
// ... |
||||
|
CoreModule |
||||
|
], |
||||
|
}) |
||||
|
export class DevExtremeModule {} |
||||
|
``` |
||||
|
|
||||
|
And change the template to the following: |
||||
|
|
||||
|
```html |
||||
|
<ng-container *ngIf="service.users$ | async as users"> |
||||
|
<dx-data-grid [dataSource]="users"> |
||||
|
<dxi-column |
||||
|
dataField="userName" |
||||
|
[caption]="'AbpIdentity::DisplayName:UserName' | abpLocalization" |
||||
|
></dxi-column> |
||||
|
<dxi-column |
||||
|
dataField="name" |
||||
|
[caption]="'AbpIdentity::DisplayName:Name' | abpLocalization" |
||||
|
></dxi-column> |
||||
|
<dxi-column |
||||
|
dataField="surname" |
||||
|
[caption]="'AbpIdentity::DisplayName:Surname' | abpLocalization" |
||||
|
></dxi-column> |
||||
|
<dxi-column |
||||
|
dataField="email" |
||||
|
[caption]="'AbpIdentity::DisplayName:Email' | abpLocalization" |
||||
|
></dxi-column> |
||||
|
<dxi-column |
||||
|
dataField="phoneNumber" |
||||
|
[caption]="'AbpIdentity::DisplayName:PhoneNumber' | abpLocalization" |
||||
|
></dxi-column> |
||||
|
</dx-data-grid> |
||||
|
</ng-container> |
||||
|
``` |
||||
|
|
||||
|
The headers should change when a new language is selected; |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Conclusion |
||||
|
|
||||
|
In this article, we have seen how to integrate `DevExtreme` angular components into a project generated by `ABP CLI`. |
||||
|
|
||||
|
You can download source code of [the demo here](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Angular). |
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 828 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 42 KiB |
@ -0,0 +1,43 @@ |
|||||
|
# Redis Cache |
||||
|
|
||||
|
ABP Framework [Caching System](Caching.md) extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). So, **any provider** supported by the standard ASP.NET Core distributed cache can be usable in your application and can be configured just like **documented by Microsoft**. |
||||
|
|
||||
|
However, ABP provides an **integration package** for Redis Cache: [Volo.Abp.Caching.StackExchangeRedis](https://www.nuget.org/packages/Volo.Abp.Caching.StackExchangeRedis). There are two reasons for using this package, instead of the standard [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis/) package. |
||||
|
|
||||
|
1. It implements `SetManyAsync` and `GetManyAsync` methods. These are not standard methods of the Microsoft Caching library, but added by the ABP Framework [Caching](Caching.md) system. They **significiantly increases the performance** when you need to set/get multiple cache items with a single method call. |
||||
|
2. It **simplifies** the Redis cache **configuration** (will be explained below). |
||||
|
|
||||
|
> Volo.Abp.Caching.StackExchangeRedis is already uses the Microsoft.Extensions.Caching.StackExchangeRedis package, but extends and improves it. |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
> This package is already installed in the application startup template if it is using Redis. |
||||
|
|
||||
|
Open a command line in the folder of your `.csproj` file and type the following ABP CLI command: |
||||
|
|
||||
|
````bash |
||||
|
abp add-package Volo.Abp.Caching.StackExchangeRedis |
||||
|
```` |
||||
|
|
||||
|
## Configuration |
||||
|
|
||||
|
Volo.Abp.Caching.StackExchangeRedis package automatically gets the redis [configuration](Configuration.md) from the `IConfiguration`. So, for example, you can set your configuration inside the `appsettings.json`: |
||||
|
|
||||
|
````js |
||||
|
"Redis": { |
||||
|
"Configuration": "127.0.0.1" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Alternatively you can configure the standard [RedisCacheOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscacheoptions) [options](Options.md) class in the `ConfigureServices` method of your [module](Module-Development-Basics.md): |
||||
|
|
||||
|
````csharp |
||||
|
Configure<RedisCacheOptions>(options => |
||||
|
{ |
||||
|
//... |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [Caching](Caching.md) |
||||
@ -0,0 +1,346 @@ |
|||||
|
# PWA Configuration |
||||
|
|
||||
|
[Progressive Web Apps](https://web.dev/progressive-web-apps/) are web applications which, although not as integrated to the OS as a native app, can take advantage of native features. They can be discovered via search engines, installed on devices with a single tap or click, and shared via a regular link. They also can work offline and get updates when new content is available. |
||||
|
|
||||
|
Converting your Angular application to a PWA is easy. |
||||
|
|
||||
|
## 1. Install Angular PWA |
||||
|
|
||||
|
Run the following command in the root folder of your Angular application: |
||||
|
|
||||
|
```shell |
||||
|
yarn ng add @angular/pwa |
||||
|
``` |
||||
|
|
||||
|
...or... |
||||
|
|
||||
|
```shell |
||||
|
npm run ng add @angular/pwa |
||||
|
``` |
||||
|
|
||||
|
This will install the `@angular/service-worker` package and make your default app a PWA. Alternatively, you may add `project` parameter to target a specific app in your workspace: |
||||
|
|
||||
|
```shell |
||||
|
yarn ng add @angular/pwa --project MyProjectName |
||||
|
``` |
||||
|
|
||||
|
Here is the output of the command: |
||||
|
|
||||
|
<img alt="Angular PWA updates and creates files" src="./images/pwa-configuration-ng-add.png" width="400px" style="max-width:100%"> |
||||
|
|
||||
|
So, Angular CLI updates some files and add a few others: |
||||
|
|
||||
|
- **ngsw-config.json** is where the [service worker configuration](https://angular.io/guide/service-worker-config) is placed. Not all PWAs have this file. It is specific to Angular. |
||||
|
- **manifest.webmanifest** is a [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) and provides information about your app in JSON format. |
||||
|
- **icons** are placeholder icons that are referred to in your web app manifest. We will replace these in a minute. |
||||
|
- **angular.json** has following modifications: |
||||
|
- `assets` include _manifest.webmanifest_. |
||||
|
- `serviceWorker` is `true` in production build. |
||||
|
- `ngswConfigPath` refers to _ngsw-config.json_. |
||||
|
- **package.json** has _@angular/service-worker_ as a new dependency. |
||||
|
- **app.module.ts** imports `ServiceWorkerModule` and registers a service worker filename. |
||||
|
- **index.html** has following modifications: |
||||
|
- A `<link>` element that refers to _manifest.webmanifest_. |
||||
|
- A `<meta>` tag that sets a theme color. |
||||
|
|
||||
|
## 2. Update the Web App Manifest |
||||
|
|
||||
|
### 2.1. Set the Name of Your App |
||||
|
|
||||
|
The `name` and the `short_name` properties in the generated manifest are derived from your project name. Let's change them. |
||||
|
|
||||
|
Open the _manifest.webmanifest_ file and update `name` and `short_name` props: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
/* rest of the manifest meta data */ |
||||
|
"short_name": "My Project", |
||||
|
"name": "My Project: My Catch-Phrase" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The short name must be really short because it will be displayed on anywhere with limited space, like the launcher and the home screen. |
||||
|
|
||||
|
### 2.2. Add a Description |
||||
|
|
||||
|
The `@angular/pwa` schematic we just added does not insert a description to your manifest file, but, according to [web app manifest standards](https://www.w3.org/TR/appmanifest/#description-member), you should. |
||||
|
|
||||
|
So, open the _manifest.webmanifest_ file and place the description as seen below: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
/* rest of the manifest meta data */ |
||||
|
"description": "My short project description giving a slightly better idea about my app" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
As a bonus, providing a description [along with other criteria](https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-edgehtml/microsoft-store#criteria-for-automatic-submission) helps Bing web crawler to index your app and automatically submit your app to Microsoft Store in `.appx` format. |
||||
|
|
||||
|
### 2.3. Set App Colors |
||||
|
|
||||
|
Angular generates the manifest file with a default `theme_color` and `background_color`. Change these according to your brand identity: |
||||
|
|
||||
|
Open the _manifest.webmanifest_ file and update `theme_color` and `background_color` properties: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
/* rest of the manifest meta data */ |
||||
|
"theme_color": "#000000", |
||||
|
"background_color": "#ffffff" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Then open _index.html_ and change the theme color meta tag as below: |
||||
|
|
||||
|
```html |
||||
|
<meta name="theme-color" content="#000000" /> |
||||
|
``` |
||||
|
|
||||
|
### 2.4. Replace App Icons & Add Splash Screens |
||||
|
|
||||
|
We need to update the icons and add some splash screens. This normally is time-consuming, but we will use the marvelous [pwa-asset-generator](https://github.com/onderceylan/pwa-asset-generator#readme) library. |
||||
|
|
||||
|
First, open the _manifest.webmanifest_ file and remove all elements in the `icons` property: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
/* rest of the manifest meta data */ |
||||
|
"icons": [] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Then, run the following command in your terminal (changing the path of course): |
||||
|
|
||||
|
```shell |
||||
|
npx pwa-asset-generator /path/to/your/logo.png ./src/assets/pwa -i ./src/index.html -m ./src/manifest.webmanifest |
||||
|
``` |
||||
|
|
||||
|
Open the _manifest.webmanifest_ file again. You will see this: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
/* rest of the manifest meta data */ |
||||
|
"icons": [ |
||||
|
{ |
||||
|
"src": "../manifest-icon-192.png", |
||||
|
"sizes": "192x192", |
||||
|
"type": "image/png", |
||||
|
"purpose": "maskable any" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "../manifest-icon-512.png", |
||||
|
"sizes": "512x512", |
||||
|
"type": "image/png", |
||||
|
"purpose": "maskable any" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In addition to updated icons, the library will generate splash screens. However, Apple requires all splash screens to be added in your _index.html_ and displays a blank screen at startup otherwise. So, the following tags will be inserted into the _index.html_ file: |
||||
|
|
||||
|
```html |
||||
|
<link |
||||
|
rel="apple-touch-icon" |
||||
|
sizes="180x180" |
||||
|
href="assets/pwa/apple-icon-180.jpg" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-icon" |
||||
|
sizes="167x167" |
||||
|
href="assets/pwa/apple-icon-167.jpg" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-icon" |
||||
|
sizes="152x152" |
||||
|
href="assets/pwa/apple-icon-152.jpg" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-icon" |
||||
|
sizes="120x120" |
||||
|
href="assets/pwa/apple-icon-120.jpg" |
||||
|
/> |
||||
|
|
||||
|
<meta name="apple-mobile-web-app-capable" content="yes" /> |
||||
|
|
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-2048-2732.jpg" |
||||
|
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-2732-2048.jpg" |
||||
|
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1668-2388.jpg" |
||||
|
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-2388-1668.jpg" |
||||
|
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1536-2048.jpg" |
||||
|
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-2048-1536.jpg" |
||||
|
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1668-2224.jpg" |
||||
|
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-2224-1668.jpg" |
||||
|
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1620-2160.jpg" |
||||
|
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-2160-1620.jpg" |
||||
|
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1242-2688.jpg" |
||||
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-2688-1242.jpg" |
||||
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1125-2436.jpg" |
||||
|
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-2436-1125.jpg" |
||||
|
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-828-1792.jpg" |
||||
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1792-828.jpg" |
||||
|
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1080-1920.jpg" |
||||
|
media="(device-width: 360px) and (device-height: 640px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1920-1080.jpg" |
||||
|
media="(device-width: 360px) and (device-height: 640px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-750-1334.jpg" |
||||
|
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1334-750.jpg" |
||||
|
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-640-1136.jpg" |
||||
|
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" |
||||
|
/> |
||||
|
<link |
||||
|
rel="apple-touch-startup-image" |
||||
|
href="assets/pwa/apple-splash-1136-640.jpg" |
||||
|
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" |
||||
|
/> |
||||
|
``` |
||||
|
|
||||
|
## 3. Configure Service Worker |
||||
|
|
||||
|
### 3.1 Modify Asset Groups |
||||
|
|
||||
|
Angular has defined some static files to be cached by the service worker, but they are not 100% accurate. Let's change it. |
||||
|
|
||||
|
Open _ngsw-config.json_ file and replace its content with this: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"$schema": "./node_modules/@angular/service-worker/config/schema.json", |
||||
|
"index": "/index.html", |
||||
|
"assetGroups": [ |
||||
|
{ |
||||
|
"name": "app", |
||||
|
"installMode": "prefetch", |
||||
|
"resources": { |
||||
|
"files": [ |
||||
|
"/favicon.ico", |
||||
|
"/index.html", |
||||
|
"/manifest.webmanifest", |
||||
|
"/*.css", |
||||
|
"/common-es2015.*.js", |
||||
|
"/main-es2015.*.js", |
||||
|
"/polyfills-es2015.*.js", |
||||
|
"/runtime-es2015.*.js", |
||||
|
"/vendor-es2015.*.js" |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"name": "modules", |
||||
|
"installMode": "lazy", |
||||
|
"updateMode": "prefetch", |
||||
|
"resources": { |
||||
|
"files": [ |
||||
|
"/*-es2015.*.js", |
||||
|
"!/common-es2015.*.js", |
||||
|
"!/main-es2015.*.js", |
||||
|
"!/polyfills-es2015.*.js", |
||||
|
"!/runtime-es2015.*.js", |
||||
|
"!/vendor-es2015.*.js" |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"name": "assets", |
||||
|
"installMode": "lazy", |
||||
|
"updateMode": "prefetch", |
||||
|
"resources": { |
||||
|
"files": [ |
||||
|
"/assets/**", |
||||
|
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In case you want to cache other static files, please refer to the [service worker configuration document](https://angular.io/guide/service-worker-config#assetgroups) on Angular.io. |
||||
|
|
||||
|
### 3.2 Set Data Groups |
||||
|
|
||||
|
This part is unique to your project. We recommend being very careful about which endpoints to cache. Please refer to [service worker configuration document](https://angular.io/guide/service-worker-config#datagroups) on Angular.io for details. |
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [Config State](./Config-State.md) |
||||
|
After Width: | Height: | Size: 247 KiB |
@ -0,0 +1,206 @@ |
|||||
|
# ASP.NET Core MVC / Razor Pages: Forms & Validation |
||||
|
|
||||
|
ABP Framework provides infrastructure and conventions to make easier to create forms, localize display names for the form elements and handle server & client side validation; |
||||
|
|
||||
|
* [abp-dynamic-form](Tag-Helpers/Dynamic-Forms.md) tag helper automates **creating a complete form** from a C# model class: Creates the input elements, handles localization and client side validation. |
||||
|
* [ABP Form tag helpers](Tag-Helpers/Form-elements.md) (`abp-input`, `abp-select`, `abp-radio`...) render **a single form element** with handling localization and client side validation. |
||||
|
* ABP Framework automatically **localizes the display name** of a form element without needing to add a `[DisplayName]` attribute. |
||||
|
* **Validation errors** are automatically localized based on the user culture. |
||||
|
|
||||
|
> This document is for the **client side validation** and it doesn't cover the server side validation. Check the [validation document](../../Validation.md) for server side validation infrastructure. |
||||
|
|
||||
|
## The Classic Way |
||||
|
|
||||
|
In a typical Bootstrap based ASP.NET Core MVC / Razor Pages UI, you [need to write](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation#client-side-validation) such a boilerplate code to create a simple form element: |
||||
|
|
||||
|
````html |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Movie.ReleaseDate" class="control-label"></label> |
||||
|
<input asp-for="Movie.ReleaseDate" class="form-control" /> |
||||
|
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span> |
||||
|
</div> |
||||
|
```` |
||||
|
|
||||
|
You can continue to use this approach if you need or prefer it. However, ABP Form tag helpers can produce the same output with a minimal code. |
||||
|
|
||||
|
## ABP Dynamic Forms |
||||
|
|
||||
|
[abp-dynamic-form](Tag-Helpers/Dynamic-Forms.md) tag helper completely automates the form creation. Take this model class as an example: |
||||
|
|
||||
|
```csharp |
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; |
||||
|
|
||||
|
namespace MyProject.Web.Pages |
||||
|
{ |
||||
|
public class MovieViewModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[StringLength(256)] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[DataType(DataType.Date)] |
||||
|
public DateTime ReleaseDate { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[TextArea] |
||||
|
[StringLength(1000)] |
||||
|
public string Description { get; set; } |
||||
|
|
||||
|
public Genre Genre { get; set; } |
||||
|
|
||||
|
public float? Price { get; set; } |
||||
|
|
||||
|
public bool PreOrder { get; set; } |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
It uses the data annotation attributes to define validation rules and UI styles for the properties. `Genre`, is an `enum` in this example: |
||||
|
|
||||
|
````csharp |
||||
|
namespace MyProject.Web.Pages |
||||
|
{ |
||||
|
public enum Genre |
||||
|
{ |
||||
|
Classic, |
||||
|
Action, |
||||
|
Fiction, |
||||
|
Fantasy, |
||||
|
Animation |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
In order to create the form in a razor page, create a property in your `PageModel` class: |
||||
|
|
||||
|
```csharp |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
|
||||
|
namespace MyProject.Web.Pages |
||||
|
{ |
||||
|
public class CreateMovieModel : PageModel |
||||
|
{ |
||||
|
[BindProperty] |
||||
|
public MovieViewModel Movie { get; set; } |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
Movie = new MovieViewModel(); |
||||
|
} |
||||
|
|
||||
|
public async Task OnPostAsync() |
||||
|
{ |
||||
|
if (ModelState.IsValid) |
||||
|
{ |
||||
|
//TODO: Save the Movie |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Then you can render the form in the `.cshtml` file: |
||||
|
|
||||
|
```html |
||||
|
@page |
||||
|
@model MyProject.Web.Pages.CreateMovieModel |
||||
|
|
||||
|
<h2>Create a new Movie</h2> |
||||
|
|
||||
|
<abp-dynamic-form abp-model="Movie" submit-button="true" /> |
||||
|
``` |
||||
|
|
||||
|
The result is shown below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
See the *Localization & Validation* section below to localize the field display names and see how the validation works. |
||||
|
|
||||
|
> See [its own document](Tag-Helpers/Dynamic-Forms.md) for all options of the `abp-dynamic-form` tag helper. |
||||
|
|
||||
|
## ABP Form Tag Helpers |
||||
|
|
||||
|
`abp-dynamic-form` covers most of the scenarios and allows you to control and customize the form using the attributes. |
||||
|
|
||||
|
However, if you want to **render the form body yourself** (for example, you may want to fully control the **form layout**), you can directly use the [ABP Form Tag Helpers](Tag-Helpers/Form-elements.md). The same auto-generated form above can be created using the ABP Form Tag Helpers as shown below: |
||||
|
|
||||
|
```html |
||||
|
@page |
||||
|
@model MyProject.Web.Pages.CreateMovieModel |
||||
|
|
||||
|
<h2>Create a new Movie</h2> |
||||
|
|
||||
|
<form method="post"> |
||||
|
<abp-input asp-for="Movie.Name"/> |
||||
|
<abp-input asp-for="Movie.ReleaseDate"/> |
||||
|
<abp-input asp-for="Movie.Description"/> |
||||
|
<abp-select asp-for="Movie.Genre"/> |
||||
|
<abp-input asp-for="Movie.Price"/> |
||||
|
<abp-input asp-for="Movie.PreOrder"/> |
||||
|
<abp-button button-type="Primary" type="submit">Save</abp-button> |
||||
|
</form> |
||||
|
``` |
||||
|
|
||||
|
> See the [ABP Form Tag Helpers](Tag-Helpers/Form-elements.md) document for details of these tag helpers and their options. |
||||
|
|
||||
|
## Validation & Localization |
||||
|
|
||||
|
Both of the Dynamic Form and the Form Tag Helpers **automatically validate** the input based on the data annotation attributes and shows validation error messages on the user interface. Error messages are **automatically localized** based on the current culture. |
||||
|
|
||||
|
**Example: User leaves empty a required string property** |
||||
|
|
||||
|
 |
||||
|
|
||||
|
The error message below is shown if the language is French: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Validation errors are already [translated](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization) a lot of languages. You can [contribute](../../Contribution/Index.md) to the translation for your own language or override the texts for your own application by following the [localization](../../Localization.md) documentation. |
||||
|
|
||||
|
## Display Name Localization |
||||
|
|
||||
|
ABP Framework uses the property name as the field name on the user interface. You typically want to [localize](../../Localization.md) this name based on the current culture. |
||||
|
|
||||
|
ABP Framework can conventionally localize the fields on the UI when you add the localization keys to the localization JSON files. |
||||
|
|
||||
|
Example: French localization for the *Name* property (add into the `fr.json` in the application): |
||||
|
|
||||
|
````js |
||||
|
"Name": "Nom" |
||||
|
```` |
||||
|
|
||||
|
Then the UI will use the given name for French language: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Using the `DisplayName:` Prefix |
||||
|
|
||||
|
Directly using the property name as the localization key may be a problem if you need to use the property name for other purpose, which a different translation value. In this case, use the `DisplayName:` prefix for the localization key: |
||||
|
|
||||
|
````js |
||||
|
"DisplayName:Name": "Nom" |
||||
|
```` |
||||
|
|
||||
|
ABP prefers to use the `DisplayName:Name` key over the `Name` key if it does exists. |
||||
|
|
||||
|
### Using a Custom Localization Key |
||||
|
|
||||
|
If you need, you can use the `[DisplayName]` attribute to specify the localization key for a specific property: |
||||
|
|
||||
|
````csharp |
||||
|
[DisplayName("MyNameKey")] |
||||
|
public string Name { get; set; } |
||||
|
```` |
||||
|
|
||||
|
In this case, you can add an entry to the localization file using the key `MyNameKey`. |
||||
|
|
||||
|
> If you use the `[DisplayName]` but not add a corresponding entity to the localization file, then ABP Framework shows the given key as the field name, `MyNameKey` for this case. So, it provides a way to specify a hard coded display name even if you don't need to use the localization system. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [Server Side Validation](../../Validation.md) |
||||
@ -1,3 +1,205 @@ |
|||||
# Navigation Menu |
# ASP.NET Core MVC / Razor Pages UI: Navigation Menu |
||||
|
|
||||
|
Every application has a main menu to allow users to navigate to pages/screens of the application. Some applications may contain more than one menu in different sections of the UI. |
||||
|
|
||||
|
ABP Framework is a [modular](../../Module-Development-Basics.md) application development framework. **Every module may need to add items to the menu**. |
||||
|
|
||||
|
So, ABP Framework **provides a menu infrastructure** where; |
||||
|
|
||||
|
* The application or the modules can add items to a menu, without knowing how the menu is rendered. |
||||
|
* The [theme](Theming.md) properly renders the menu. |
||||
|
|
||||
|
## Adding Menu Items |
||||
|
|
||||
|
In order to add menu items (or manipulate the existing items) you need to create a class implementing the `IMenuContributor` interface. |
||||
|
|
||||
|
**Example: Add a *CRM* menu item with *Customers* and *Orders* sub menu items** |
||||
|
|
||||
|
```csharp |
||||
|
using System.Threading.Tasks; |
||||
|
using MyProject.Localization; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
|
namespace MyProject.Web.Menus |
||||
|
{ |
||||
|
public class MyProjectMenuContributor : IMenuContributor |
||||
|
{ |
||||
|
public async Task ConfigureMenuAsync(MenuConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Menu.Name == StandardMenus.Main) |
||||
|
{ |
||||
|
await ConfigureMainMenuAsync(context); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) |
||||
|
{ |
||||
|
var l = context.GetLocalizer<MyProjectResource>(); |
||||
|
|
||||
|
context.Menu.AddItem( |
||||
|
new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"]) |
||||
|
.AddItem(new ApplicationMenuItem( |
||||
|
name: "MyProject.Crm.Customers", |
||||
|
displayName: l["Menu:Customers"], |
||||
|
url: "/crm/customers") |
||||
|
).AddItem(new ApplicationMenuItem( |
||||
|
name: "MyProject.Crm.Orders", |
||||
|
displayName: l["Menu:Orders"], |
||||
|
url: "/crm/orders") |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
* This example adds items only to the main menu (`StandardMenus.Main`: see the *Standard Menus* section below). |
||||
|
* It gets a `IStringLocalizer` from `context` to [localize](../../Localization.md) the display names of the menu items. |
||||
|
* Adds the Customers and Orders as children of the CRM menu. |
||||
|
|
||||
|
Once you create a menu contributor, you need to add it to the `AbpNavigationOptions` in the `ConfigureServices` method of your module: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<AbpNavigationOptions>(options => |
||||
|
{ |
||||
|
options.MenuContributors.Add(new MyProjectMenuContributor()); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
When you run the application, you will see the menu items added to the main menu: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
> The menu is rendered by the current UI theme. So, the look of the main menu can be completely different based on your theme. |
||||
|
|
||||
|
Here, a few notes on the menu contributors; |
||||
|
|
||||
|
* ABP Framework calls the `ConfigureMenuAsync` method **whenever need to render** the menu. |
||||
|
* Every menu item can have **children**. So, you can add menu items with **unlimited depth** (however, your UI theme may not support unlimited depth). |
||||
|
* Only leaf menu items have `url`s normally. When you click to a parent menu, its sub menu is opened or closed, you don't navigate the `url` of a parent menu item. |
||||
|
* If a menu item has no children and has no `url` defined, then it is not rendered on the UI. This simplifies to authorize the menu items: You only authorize the child items (see the next section). If none of the children are authorized, then the parent automatically disappears. |
||||
|
|
||||
|
### Menu Item Properties |
||||
|
|
||||
|
There are more options of a menu item (the constructor of the `ApplicationMenuItem` class). Here, the list of all available options; |
||||
|
|
||||
|
* `name` (`string`, required): The unique name of the menu item. |
||||
|
* `displayName` (`string`, required): Display name/text of the menu item. You can [localize](../../Localization.md) this as shown before. |
||||
|
* `url` (`string`): The URL of the menu item. |
||||
|
* `icon` (`string`): An icon name. Free [Font Awesome](https://fontawesome.com/) icon classes are supported out of the box. Example: `fa fa-book`. You can use any CSS font icon class as long as you include the necessary CSS files to your application. |
||||
|
* `order` (`int`): The order of the menu item. Default value is `1000`. Items are sorted by the adding order unless you specify an order value. |
||||
|
* `customData` (`object`): A custom object that you can associate to the menu item and use it while rendering the menu item. |
||||
|
* `target` (`string`): Target of the menu item. Can be `null` (default), "_blank", "_*self*", "_parent", "_*top*" or a frame name for web applications. |
||||
|
* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute. |
||||
|
* `cssClass` (`string`): Additional string classes for the menu item. |
||||
|
|
||||
|
### Authorization |
||||
|
|
||||
|
As seen above, a menu contributor contributes to the menu dynamically. So, you can perform any custom logic or get menu items from any source. |
||||
|
|
||||
|
One use case is the [authorization](Authorization.md). You typically want to add menu items by checking a permission. |
||||
|
|
||||
|
**Example: Check if the current user has a permission** |
||||
|
|
||||
|
````csharp |
||||
|
if (await context.IsGrantedAsync("MyPermissionName")) |
||||
|
{ |
||||
|
//...add menu items |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> You can use `context.AuthorizationService` to directly access to the `IAuthorizationService`. |
||||
|
|
||||
|
### Resolving Dependencies |
||||
|
|
||||
|
`context.ServiceProvider` can be used to resolve any service dependency. |
||||
|
|
||||
|
**Example: Get a service** |
||||
|
|
||||
|
````csharp |
||||
|
var myService = context.ServiceProvider.GetRequiredService<IMyService>(); |
||||
|
//...use the service |
||||
|
```` |
||||
|
|
||||
|
> You don't need to care about releasing/disposing services. ABP Framework handles it. |
||||
|
|
||||
|
### The Administration Menu |
||||
|
|
||||
|
There is a special menu item in the menu menu that is added by the ABP Framework: The *Administration* menu. It is typically used by the pre-built admin [application modules](../../Modules/Index.md): |
||||
|
|
||||
|
 |
||||
|
|
||||
|
If you want to add menu items under the *Administration* menu item, you can use the `context.Menu.GetAdministration()` extension method: |
||||
|
|
||||
|
````csharp |
||||
|
context.Menu.GetAdministration().AddItem(...) |
||||
|
```` |
||||
|
|
||||
|
### Manipulating the Existing Menu Items |
||||
|
|
||||
|
ABP Framework executes the menu contributors by the [module dependency order](../../Module-Development-Basics.md). So, you can manipulate the menu items that your application or module (directly or indirectly) depends on. |
||||
|
|
||||
|
**Example: Set an icon for the `Users` menu item added by the [Identity Module](../../Modules/Identity.md)** |
||||
|
|
||||
|
````csharp |
||||
|
var userMenu = context.Menu.FindMenuItem(IdentityMenuNames.Users); |
||||
|
userMenu.Icon = "fa fa-users"; |
||||
|
```` |
||||
|
|
||||
|
> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors. |
||||
|
|
||||
|
## Standard Menus |
||||
|
|
||||
|
A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus: |
||||
|
|
||||
|
* `Main`: The main menu of the application. Contains links to the page of the application. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.Main`. |
||||
|
* `User`: User profile menu. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.User`. |
||||
|
|
||||
|
The `Main` menu already covered above. The `User` menu is available when a user has logged in: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
You can add items to the `User` menu by checking the `context.Menu.Name` as shown below: |
||||
|
|
||||
|
```csharp |
||||
|
if (context.Menu.Name == StandardMenus.User) |
||||
|
{ |
||||
|
//...add items |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## IMenuManager |
||||
|
|
||||
|
`IMenuManager` is generally used by the UI [theme](Theming.md) to render the menu items on the UI. So, **you generally don't need to directly use** the `IMenuManager`. |
||||
|
|
||||
|
**Example: Getting the `Main` menu items** |
||||
|
|
||||
|
```csharp |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
|
namespace MyProject.Web.Pages |
||||
|
{ |
||||
|
public class IndexModel : PageModel |
||||
|
{ |
||||
|
private readonly IMenuManager _menuManager; |
||||
|
|
||||
|
public IndexModel(IMenuManager menuManager) |
||||
|
{ |
||||
|
_menuManager = menuManager; |
||||
|
} |
||||
|
|
||||
|
public async Task OnGetAsync() |
||||
|
{ |
||||
|
var mainMenu = await _menuManager.GetAsync(StandardMenus.Main); |
||||
|
|
||||
|
foreach (var menuItem in mainMenu.Items) |
||||
|
{ |
||||
|
//... |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
TODO |
|
||||
@ -0,0 +1,84 @@ |
|||||
|
# ASP.NET Core MVC / Razor Pages: Page Alerts |
||||
|
|
||||
|
It is common to show error, warning or information alerts to inform the user. An example *Service Interruption* alert is shown below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Basic Usage |
||||
|
|
||||
|
If you directly or indirectly inherit from `AbpPageModel`, you can use the `Alerts` property to add alerts to be rendered after the request completes. |
||||
|
|
||||
|
**Example: Show a Warning alert** |
||||
|
|
||||
|
```csharp |
||||
|
namespace MyProject.Web.Pages |
||||
|
{ |
||||
|
public class IndexModel : MyProjectPageModel //or inherit from AbpPageModel |
||||
|
{ |
||||
|
public void OnGet() |
||||
|
{ |
||||
|
Alerts.Warning( |
||||
|
text: "We will have a service interruption between 02:00 AM and 04:00 AM at October 23, 2023!", |
||||
|
title: "Service Interruption" |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
This usage renders an alert that was shown above. If you need to localize the messages, you can always use the standard [localization](../../Localization.md) system. |
||||
|
|
||||
|
### Exceptions / Invalid Model States |
||||
|
|
||||
|
It is typical to show alerts when you manually handle exceptions (with try/catch statements) or want to handle `!ModelState.IsValid` case and warn the user. For example, the Account Module shows a warning if user enters an incorrect username or password: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
> Note that you generally don't need to manually handle exceptions since ABP Framework provides an automatic [exception handling](../../Exception-Handling.md) system. |
||||
|
|
||||
|
### Alert Types |
||||
|
|
||||
|
`Warning` is used to show a warning alert. Other common methods are `Info`, `Danger` and `Success`. |
||||
|
|
||||
|
Beside the standard methods, you can use the `Alerts.Add` method by passing an `AlertType` `enum` with one of these values: `Default`, `Primary`, `Secondary`, `Success`, `Danger`, `Warning`, `Info`, `Light`, `Dark`. |
||||
|
|
||||
|
### Dismissible |
||||
|
|
||||
|
All alert methods gets an optional `dismissible` parameter. Default value is `true` which makes the alert box dismissible. Set it to `false` to create a sticky alert box. |
||||
|
|
||||
|
## IAlertManager |
||||
|
|
||||
|
If you need to add alert messages from another part of your code, you can inject the `IAlertManager` service and use its `Alerts` list. |
||||
|
|
||||
|
**Example: Inject the `IAlertManager`** |
||||
|
|
||||
|
```csharp |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Alerts; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace MyProject.Web.Pages |
||||
|
{ |
||||
|
public class MyService : ITransientDependency |
||||
|
{ |
||||
|
private readonly IAlertManager _alertManager; |
||||
|
|
||||
|
public MyService(IAlertManager alertManager) |
||||
|
{ |
||||
|
_alertManager = alertManager; |
||||
|
} |
||||
|
|
||||
|
public void Test() |
||||
|
{ |
||||
|
_alertManager.Alerts.Add(AlertType.Danger, "Test message!"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Notes |
||||
|
|
||||
|
### AJAX Requests |
||||
|
|
||||
|
Page Alert system was designed to be used in a regular full page request. It is not for AJAX/partial requests. The alerts are rendered in the page layout, so a full page refresh is needed. |
||||
|
|
||||
|
For AJAX requests, it is more proper to throw exceptions (e.g. `UserFriendlyException`). See the [exception handling](../../Exception-Handling.md) document. |
||||
@ -0,0 +1,6 @@ |
|||||
|
# Blazor UI for the ABP Framework |
||||
|
|
||||
|
The detailed documentation for the Blazor UI is in progress. However, you can follow the documents below to start with the Blazor UI today. |
||||
|
|
||||
|
* [Get started](https://docs.abp.io/en/abp/latest/Getting-Started?UI=Blazor) with the Blazor UI for the ABP Framework. |
||||
|
* [Web Application Development Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=Blazor) with the Blazor UI. |
||||
@ -0,0 +1,3 @@ |
|||||
|
# Blazor UI Notification |
||||
|
|
||||
|
`UiNotificationService` is used to show toastr style notifications on the user interface. The documentation is in progress... |
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
@ -0,0 +1,131 @@ |
|||||
|
# Rules in this file were initially inferred by Visual Studio IntelliCode from the D:\Projects\Volosoft\abp\framework codebase based on best match to current usage at 2.10.2020. |
||||
|
# You can modify the rules from these initially generated values to suit your own policies |
||||
|
# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference |
||||
|
[*.cs] |
||||
|
|
||||
|
|
||||
|
#Core editorconfig formatting - indentation |
||||
|
|
||||
|
#use soft tabs (spaces) for indentation |
||||
|
indent_style = space |
||||
|
|
||||
|
#Formatting - indentation options |
||||
|
|
||||
|
#indent switch case contents. |
||||
|
csharp_indent_case_contents = true |
||||
|
#indent switch labels |
||||
|
csharp_indent_switch_labels = true |
||||
|
|
||||
|
#Formatting - new line options |
||||
|
|
||||
|
#place catch statements on a new line |
||||
|
csharp_new_line_before_catch = true |
||||
|
#place else statements on a new line |
||||
|
csharp_new_line_before_else = true |
||||
|
#require members of object intializers to be on separate lines |
||||
|
csharp_new_line_before_members_in_object_initializers = true |
||||
|
#require braces to be on a new line for accessors, methods, lambdas, object_collection_array_initializers, control_blocks, types, and properties (also known as "Allman" style) |
||||
|
csharp_new_line_before_open_brace = accessors, methods, lambdas, object_collection_array_initializers, control_blocks, types, properties |
||||
|
|
||||
|
#Formatting - organize using options |
||||
|
|
||||
|
#sort System.* using directives alphabetically, and place them before other usings |
||||
|
dotnet_sort_system_directives_first = true |
||||
|
|
||||
|
#Formatting - spacing options |
||||
|
|
||||
|
csharp_space_after_cast = false |
||||
|
csharp_space_after_colon_in_inheritance_clause = true |
||||
|
csharp_space_after_comma = true |
||||
|
csharp_space_after_dot = false |
||||
|
csharp_space_after_keywords_in_control_flow_statements = true |
||||
|
csharp_space_after_semicolon_in_for_statement = true |
||||
|
csharp_space_around_binary_operators = before_and_after |
||||
|
csharp_space_around_declaration_statements = false |
||||
|
csharp_space_before_colon_in_inheritance_clause = true |
||||
|
csharp_space_before_comma = false |
||||
|
csharp_space_before_dot = false |
||||
|
csharp_space_before_open_square_brackets = false |
||||
|
csharp_space_before_semicolon_in_for_statement = false |
||||
|
csharp_space_between_empty_square_brackets = false |
||||
|
csharp_space_between_method_call_empty_parameter_list_parentheses = false |
||||
|
csharp_space_between_method_call_name_and_opening_parenthesis = false |
||||
|
csharp_space_between_method_call_parameter_list_parentheses = false |
||||
|
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false |
||||
|
csharp_space_between_method_declaration_name_and_open_parenthesis = false |
||||
|
csharp_space_between_method_declaration_parameter_list_parentheses = false |
||||
|
csharp_space_between_parentheses = false |
||||
|
csharp_space_between_square_brackets = false |
||||
|
|
||||
|
#Formatting - wrapping options |
||||
|
|
||||
|
#leave code block on single line |
||||
|
csharp_preserve_single_line_blocks = true |
||||
|
|
||||
|
#Style - Code block preferences |
||||
|
|
||||
|
#prefer curly braces even for one line of code |
||||
|
csharp_prefer_braces = true:suggestion |
||||
|
|
||||
|
#Style - expression bodied member options |
||||
|
|
||||
|
#prefer block bodies for constructors |
||||
|
csharp_style_expression_bodied_constructors = false:suggestion |
||||
|
#prefer block bodies for methods |
||||
|
csharp_style_expression_bodied_methods = false:suggestion |
||||
|
#prefer expression-bodied members for properties |
||||
|
csharp_style_expression_bodied_properties = true:suggestion |
||||
|
|
||||
|
#Style - expression level options |
||||
|
|
||||
|
#prefer out variables to be declared inline in the argument list of a method call when possible |
||||
|
csharp_style_inlined_variable_declaration = true:suggestion |
||||
|
#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them |
||||
|
dotnet_style_predefined_type_for_member_access = true:suggestion |
||||
|
|
||||
|
#Style - Expression-level preferences |
||||
|
|
||||
|
#prefer default over default(T) |
||||
|
csharp_prefer_simple_default_expression = true:suggestion |
||||
|
#prefer objects to be initialized using object initializers when possible |
||||
|
dotnet_style_object_initializer = true:suggestion |
||||
|
#prefer inferred tuple element names |
||||
|
dotnet_style_prefer_inferred_tuple_names = true:suggestion |
||||
|
|
||||
|
#Style - implicit and explicit types |
||||
|
|
||||
|
#prefer var over explicit type in all cases, unless overridden by another code style rule |
||||
|
csharp_style_var_elsewhere = true:suggestion |
||||
|
#prefer var is used to declare variables with built-in system types such as int |
||||
|
csharp_style_var_for_built_in_types = true:suggestion |
||||
|
#prefer var when the type is already mentioned on the right-hand side of a declaration expression |
||||
|
csharp_style_var_when_type_is_apparent = true:suggestion |
||||
|
|
||||
|
#Style - language keyword and framework type options |
||||
|
|
||||
|
#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them |
||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion |
||||
|
|
||||
|
#Style - modifier options |
||||
|
|
||||
|
#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. |
||||
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion |
||||
|
|
||||
|
#Style - Modifier preferences |
||||
|
|
||||
|
#when this rule is set to a list of modifiers, prefer the specified ordering. |
||||
|
csharp_preferred_modifier_order = public,protected,private,virtual,async,static,override,readonly,abstract:suggestion |
||||
|
|
||||
|
#Style - Pattern matching |
||||
|
|
||||
|
#prefer pattern matching instead of is expression with type casts |
||||
|
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion |
||||
|
|
||||
|
#Style - qualification options |
||||
|
|
||||
|
#prefer fields not to be prefaced with this. or Me. in Visual Basic |
||||
|
dotnet_style_qualification_for_field = false:suggestion |
||||
|
#prefer methods not to be prefaced with this. or Me. in Visual Basic |
||||
|
dotnet_style_qualification_for_method = false:suggestion |
||||
|
#prefer properties not to be prefaced with this. or Me. in Visual Basic |
||||
|
dotnet_style_qualification_for_property = false:suggestion |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic; |
||||
|
using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme |
||||
|
{ |
||||
|
public class BasicThemeToolbarContributor : IToolbarContributor |
||||
|
{ |
||||
|
public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Toolbar.Name == StandardToolbars.Main) |
||||
|
{ |
||||
|
context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitch))); |
||||
|
context.Toolbar.Items.Add(new ToolbarItem(typeof(LoginDisplay))); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,18 +1,50 @@ |
|||||
using System.Threading.Tasks; |
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Localization.Resources.AbpUi; |
||||
using Microsoft.AspNetCore.Components; |
using Microsoft.AspNetCore.Components; |
||||
|
using Microsoft.AspNetCore.Components.Routing; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.Http.Client; |
||||
using Volo.Abp.UI.Navigation; |
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic |
namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic |
||||
{ |
{ |
||||
public partial class LoginDisplay |
public partial class LoginDisplay : IDisposable |
||||
{ |
{ |
||||
[Inject] protected IMenuManager MenuManager { get; set; } |
[Inject] |
||||
|
protected IMenuManager MenuManager { get; set; } |
||||
|
|
||||
|
[Inject] |
||||
|
protected IStringLocalizer<AbpUiResource> UiLocalizer { get; set; } |
||||
|
|
||||
|
[Inject] |
||||
|
protected IOptions<AbpRemoteServiceOptions> RemoteServiceOptions { get; set; } |
||||
|
|
||||
protected ApplicationMenu Menu { get; set; } |
protected ApplicationMenu Menu { get; set; } |
||||
|
|
||||
|
protected string ServerUrl { get; set; } |
||||
|
protected string ServerAccountUrl { get; set; } |
||||
|
|
||||
protected override async Task OnInitializedAsync() |
protected override async Task OnInitializedAsync() |
||||
{ |
{ |
||||
Menu = await MenuManager.GetAsync(StandardMenus.User); |
Menu = await MenuManager.GetAsync(StandardMenus.User); |
||||
|
|
||||
|
ServerUrl = RemoteServiceOptions.Value.RemoteServices.Default?.BaseUrl?.TrimEnd('/'); |
||||
|
ServerAccountUrl = ServerUrl + "/Account/Manage?returnUrl=" + Navigation.Uri; |
||||
|
|
||||
|
Navigation.LocationChanged += OnLocationChanged; |
||||
|
} |
||||
|
|
||||
|
protected virtual void OnLocationChanged(object sender, LocationChangedEventArgs e) |
||||
|
{ |
||||
|
ServerAccountUrl = ServerUrl + "/Account/Manage?returnUrl=" + Navigation.Uri; |
||||
|
StateHasChanged(); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
Navigation.LocationChanged -= OnLocationChanged; |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
<ul class="navbar-nav"> |
||||
|
@foreach (var render in ToolbarItemRenders) |
||||
|
{ |
||||
|
<li class="nav-item"> |
||||
|
@render |
||||
|
</li> |
||||
|
} |
||||
|
</ul> |
||||
@ -0,0 +1,32 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Components; |
||||
|
using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic |
||||
|
{ |
||||
|
public partial class NavToolbar |
||||
|
{ |
||||
|
[Inject] |
||||
|
private IToolbarManager ToolbarManager { get; set; } |
||||
|
|
||||
|
private List<RenderFragment> ToolbarItemRenders { get; set; } = new List<RenderFragment>(); |
||||
|
|
||||
|
protected override async Task OnInitializedAsync() |
||||
|
{ |
||||
|
var toolbar = await ToolbarManager.GetAsync(StandardToolbars.Main); |
||||
|
|
||||
|
ToolbarItemRenders.Clear(); |
||||
|
|
||||
|
foreach (var item in toolbar.Items) |
||||
|
{ |
||||
|
ToolbarItemRenders.Add(builder => |
||||
|
{ |
||||
|
builder.OpenComponent(0, item.ComponentType); |
||||
|
builder.CloseComponent(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; |
||||
|
using Volo.Abp; |
||||
|
|
||||
|
namespace Microsoft.Extensions.DependencyInjection |
||||
|
{ |
||||
|
public static class AbpWebAssemblyServiceCollectionExtensions |
||||
|
{ |
||||
|
public static WebAssemblyHostBuilder GetHostBuilder( |
||||
|
[NotNull] this IServiceCollection services) |
||||
|
{ |
||||
|
Check.NotNull(services, nameof(services)); |
||||
|
|
||||
|
return services.GetSingletonInstance<WebAssemblyHostBuilder>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
using System; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly.ExceptionHandling |
||||
|
{ |
||||
|
public class AbpExceptionHandlingLogger : ILogger, IDisposable |
||||
|
{ |
||||
|
private readonly IServiceCollection _serviceCollection; |
||||
|
private IServiceScope _serviceScope; |
||||
|
private IUserExceptionInformer _userExceptionInformer; |
||||
|
|
||||
|
public AbpExceptionHandlingLogger(IServiceCollection serviceCollection) |
||||
|
{ |
||||
|
_serviceCollection = serviceCollection; |
||||
|
} |
||||
|
|
||||
|
public virtual void Log<TState>( |
||||
|
LogLevel logLevel, |
||||
|
EventId eventId, |
||||
|
TState state, |
||||
|
Exception exception, |
||||
|
Func<TState, Exception, string> formatter) |
||||
|
{ |
||||
|
if (exception == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (logLevel != LogLevel.Critical && logLevel != LogLevel.Error) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
TryInitialize(); |
||||
|
|
||||
|
if (_userExceptionInformer == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_userExceptionInformer.InformAsync(new UserExceptionInformerContext(exception)); |
||||
|
} |
||||
|
|
||||
|
protected virtual void TryInitialize() |
||||
|
{ |
||||
|
var serviceProvider = _serviceCollection.GetServiceProviderOrNull(); |
||||
|
if (serviceProvider == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_serviceScope = serviceProvider.CreateScope(); |
||||
|
_userExceptionInformer = _serviceScope.ServiceProvider.GetRequiredService<IUserExceptionInformer>(); |
||||
|
} |
||||
|
|
||||
|
public virtual bool IsEnabled(LogLevel logLevel) |
||||
|
{ |
||||
|
return logLevel == LogLevel.Critical || logLevel == LogLevel.Error; |
||||
|
} |
||||
|
|
||||
|
public virtual IDisposable BeginScope<TState>(TState state) |
||||
|
{ |
||||
|
return NullDisposable.Instance; |
||||
|
} |
||||
|
|
||||
|
public virtual void Dispose() |
||||
|
{ |
||||
|
_serviceScope?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly.ExceptionHandling |
||||
|
{ |
||||
|
public class AbpExceptionHandlingLoggerProvider : ILoggerProvider |
||||
|
{ |
||||
|
private AbpExceptionHandlingLogger _logger; |
||||
|
private static readonly object SyncObj = new object(); |
||||
|
private readonly IServiceCollection _serviceCollection; |
||||
|
|
||||
|
public AbpExceptionHandlingLoggerProvider(IServiceCollection serviceCollection) |
||||
|
{ |
||||
|
_serviceCollection = serviceCollection; |
||||
|
} |
||||
|
|
||||
|
public ILogger CreateLogger(string categoryName) |
||||
|
{ |
||||
|
if (_logger == null) |
||||
|
{ |
||||
|
lock (SyncObj) |
||||
|
{ |
||||
|
if (_logger == null) |
||||
|
{ |
||||
|
_logger = new AbpExceptionHandlingLogger(_serviceCollection); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return _logger; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_logger.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly.ExceptionHandling |
||||
|
{ |
||||
|
public interface IUserExceptionInformer |
||||
|
{ |
||||
|
Task InformAsync(UserExceptionInformerContext context); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.AspNetCore.ExceptionHandling; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Http; |
||||
|
using Volo.Abp.Http.Client; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly.ExceptionHandling |
||||
|
{ |
||||
|
public class UserExceptionInformer : IUserExceptionInformer, ITransientDependency |
||||
|
{ |
||||
|
protected IUiMessageService MessageService { get; } |
||||
|
protected IExceptionToErrorInfoConverter ExceptionToErrorInfoConverter { get; } |
||||
|
|
||||
|
public UserExceptionInformer(IUiMessageService messageService, IExceptionToErrorInfoConverter exceptionToErrorInfoConverter) |
||||
|
{ |
||||
|
MessageService = messageService; |
||||
|
ExceptionToErrorInfoConverter = exceptionToErrorInfoConverter; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task InformAsync(UserExceptionInformerContext context) |
||||
|
{ |
||||
|
var errorInfo = GetErrorInfo(context); |
||||
|
await ShowErrorInfoAsync(errorInfo); |
||||
|
} |
||||
|
|
||||
|
protected virtual RemoteServiceErrorInfo GetErrorInfo(UserExceptionInformerContext context) |
||||
|
{ |
||||
|
if (context.Exception is AbpRemoteCallException remoteCallException) |
||||
|
{ |
||||
|
return remoteCallException.Error; |
||||
|
} |
||||
|
|
||||
|
return ExceptionToErrorInfoConverter.Convert(context.Exception, false); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task ShowErrorInfoAsync(RemoteServiceErrorInfo errorInfo) |
||||
|
{ |
||||
|
if (errorInfo.Details.IsNullOrEmpty()) |
||||
|
{ |
||||
|
await MessageService.ErrorAsync(errorInfo.Message); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
await MessageService.ErrorAsync(errorInfo.Details, errorInfo.Message); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System; |
||||
|
using JetBrains.Annotations; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly.ExceptionHandling |
||||
|
{ |
||||
|
public class UserExceptionInformerContext |
||||
|
{ |
||||
|
[NotNull] |
||||
|
public Exception Exception { get; } |
||||
|
|
||||
|
public UserExceptionInformerContext(Exception exception) |
||||
|
{ |
||||
|
Exception = exception; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly |
||||
|
{ |
||||
|
public interface IUiNotificationService |
||||
|
{ |
||||
|
Task Info(string message); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Components.WebAssembly |
||||
|
{ |
||||
|
public class NullUiNotificationService : IUiNotificationService, ITransientDependency |
||||
|
{ |
||||
|
public Task Info(string message) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using System.IO; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using Microsoft.AspNetCore.Mvc.Rendering; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers |
||||
|
{ |
||||
|
internal static class TagBuilderExtensions |
||||
|
{ |
||||
|
public static string ToHtmlString(this TagBuilder tagBuilder) |
||||
|
{ |
||||
|
return tagBuilder.ToHtmlString(HtmlEncoder.Default); |
||||
|
} |
||||
|
|
||||
|
public static string ToHtmlString(this TagBuilder tagBuilder, HtmlEncoder htmlEncoder) |
||||
|
{ |
||||
|
using (var writer = new StringWriter()) |
||||
|
{ |
||||
|
tagBuilder.WriteTo(writer, htmlEncoder); |
||||
|
return writer.ToString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||