@ -0,0 +1,88 @@ |
|||
# Upgrading the Startup Template |
|||
|
|||
Sometimes we introduce new features/changes that requires to **make changes in the startup template**. We already implement the changes in the startup template for new applications. However, in some cases you need to manually make some minor changes in your existing solution. |
|||
|
|||
This guide explains a suggested way of upgrading your solution templates, using the WinMerge tool. |
|||
|
|||
> See also the [Upgrading document](../Upgrading.md) for an overall progress of upgrading. This document focuses on upgrading the startup template. |
|||
|
|||
## 1) Create Dummy Solutions |
|||
|
|||
We will create two solutions to compare the changes; |
|||
|
|||
* The first solution is with your existing version |
|||
* The second solution is the version you want to upgrade |
|||
|
|||
Assume that we are upgrading from the version **4.2.2** to version **4.3.0-rc.1**. First, create two empty folders: |
|||
|
|||
 |
|||
|
|||
**A)** Open a command-line terminal inside the `4_2_2` folder and create a new solution with the version `4.2.2` using the ABP [CLI](../CLI.md) (install it if you haven't installed before). |
|||
|
|||
**Example:** |
|||
|
|||
````bash |
|||
abp new MyCompareApp -u blazor -v 4.2.2 |
|||
```` |
|||
|
|||
> Important: You need to create the solution with the exact configuration of your solution. If your application has Angular UI and MongoDB, you should use the same options here. |
|||
|
|||
**B)** Then open a command-line terminal inside the `4_3_0-rc1` folder and create a new solution with the version `4.3.0-rc.1` using the ABP [CLI](../CLI.md). |
|||
|
|||
**Example:** |
|||
|
|||
````bash |
|||
abp new MyCompareApp -u blazor -v 4.3.0-rc.1 |
|||
```` |
|||
|
|||
Now, we have the same application with different versions. |
|||
|
|||
## 2) Upgrade the Old Application |
|||
|
|||
If we compare two folders now, we will see unnecessary differences because of NuGet & NPM package differences. It is better to upgrade the old application to the new version before comparing them. |
|||
|
|||
Open a command-line terminal inside the `4_2_2` folder and type the following command: |
|||
|
|||
````bash |
|||
abp update -v 4.3.0-rc.1 |
|||
```` |
|||
|
|||
This will update all NuGet & NPM packages in your solution. We are ready to compare the folders to see the differences. |
|||
|
|||
## 3) Compare the Folders |
|||
|
|||
We will use the [WinMerge](https://winmerge.org/) utility for the comparison. So, please install it if it wasn't installed before. After installation, open the WinMerge application, select the the *File > Open* menu item, select the folders you want to compare: |
|||
|
|||
 |
|||
|
|||
Now, we can click to the *Compare* button to see all the differences. Here, a screenshot from the comparison: |
|||
|
|||
 |
|||
|
|||
See the *Comparison result* column or the yellow coloring to understand if two files or folder are different. It shows almost all folders are different. However, don't worry. Generally a few files will be different in a folder and a few lines will be different in a file comparison. |
|||
|
|||
For example, I select the `MyCompareApp.Blazor.csproj` to understand what's changed in this file: |
|||
|
|||
 |
|||
|
|||
We see that; |
|||
|
|||
* `Blazorise.Bootstrap` package is upgraded from version `0.9.3-preview6` to version `0.9.3.3`. |
|||
* `Blazorise.Icons.FontAwesome` package is upgraded from version `0.9.3-preview6` to version `0.9.3.3`. |
|||
* `Volo.Abp.Identity.Blazor` package is replaced by `Volo.Abp.Identity.Blazor.WebAssembly`. |
|||
* `Volo.Abp.TenantManagement.Blazor` package is replaced by `Volo.Abp.TenantManagement.Blazor.WebAssembly`. |
|||
* `Volo.Abp.SettingManagement.Blazor.WebAssembly` package is newly added. |
|||
|
|||
In this way, we can understand all the changes. |
|||
|
|||
## 4) Apply Changes on Your Solution |
|||
|
|||
Comparison result clearly shows the necessary changes should be done on upgrade. All you need to do is to apply the same changes in your own solution. |
|||
|
|||
> **It is important you first upgrade your own solution to the new version, using the `abp update` command. Then you can apply the manual changes** |
|||
|
|||
## Notes |
|||
|
|||
* Sometimes, you may find some changes are unnecessary for your own solution. You may deleted these or already customized. In these cases, you can just ignore it. |
|||
* If you do not upgrade your solution as described in this document, your application will continue to work as long as you implement the breaking changes documented in the [migration guide](Index.md). However, you may not get benefit of some new features those require changes in your solution files. |
|||
* Most of the times, there will be a few or no differences on the startup templates. When there are important changes, we write a note to the related migration guide, so you apply them manually. |
|||
@ -1,3 +0,0 @@ |
|||
# Blogging Module |
|||
|
|||
TODO |
|||
@ -1,3 +0,0 @@ |
|||
# Client Simulation Module |
|||
|
|||
TODO |
|||
@ -1,3 +0,0 @@ |
|||
# Users Module |
|||
|
|||
TODO |
|||
@ -1,3 +1,111 @@ |
|||
# Emailing |
|||
# SMS Sending |
|||
|
|||
TODO! |
|||
The ABP Framework provides an abstraction to sending SMS. Having such an abstraction has some benefits; |
|||
|
|||
- You can then **easily change** your SMS sender without changing your application code. |
|||
- If you want to create **reusable application modules**, you don't need to make assumption about how the SMS are sent. |
|||
|
|||
## Installation |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. |
|||
|
|||
### Using the ABP CLI |
|||
|
|||
Open a command line window in the folder of the project (.csproj file) and type the following command: |
|||
|
|||
```bash |
|||
abp add-package Volo.Abp.Sms |
|||
``` |
|||
|
|||
### Manual Installation |
|||
|
|||
If you want to manually install; |
|||
|
|||
1. Add the [Volo.Abp.Sms](https://www.nuget.org/packages/Volo.Abp.Sms) NuGet package to your project: |
|||
|
|||
``` |
|||
Install-Package Volo.Abp.Sms |
|||
``` |
|||
|
|||
2. Add the `AbpSmsModule` to the dependency list of your module: |
|||
|
|||
```csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpSmsModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
``` |
|||
|
|||
## Sending SMS |
|||
|
|||
[Inject](Dependency-Injection.md) the `ISmsSender` into any service and use the `SendAsync` method to send a SMS. |
|||
|
|||
**Example:** |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Sms; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ISmsSender _smsSender; |
|||
|
|||
public MyService(ISmsSender smsSender) |
|||
{ |
|||
_smsSender = smsSender; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
await _smsSender.SendAsync( |
|||
"+012345678901", // target phone number |
|||
"This is test sms..." // message text |
|||
); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The given `SendAsync` method in the example is an extension method to send an SMS with primitive parameters. In addition, you can pass an `SmsMessage` object which has the following properties: |
|||
|
|||
- `PhoneNumber` (`string`): Target phone number |
|||
- `Text` (`string`): Message text |
|||
- `Properties` (`Dictionary<string, string>`): Key-value pairs to pass custom arguments |
|||
|
|||
## NullSmsSender |
|||
|
|||
`NullSmsSender` is a the default implementation of the `ISmsSender`. It writes SMS content to the [standard logler](Logging.md), rather than actually sending the SMS. |
|||
|
|||
This class can be useful especially in development time where you generally don't want to send real SMS. **However, if you want to actually send SMS, you should implement the `ISmsSender` in your application code.** |
|||
|
|||
## Implementing the ISmsSender |
|||
|
|||
You can easily create your SMS sending implementation by creating a class that implements the `ISmsSender` interface, as shown below: |
|||
|
|||
```csharp |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Sms; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyCustomSmsSender : ISmsSender, ITransientDependency |
|||
{ |
|||
public async Task SendAsync(SmsMessage smsMessage) |
|||
{ |
|||
// Send sms |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## More |
|||
|
|||
[ABP Commercial](https://commercial.abp.io/) provides Twilio integration package to send SMS over [Twilio service](https://docs.abp.io/en/commercial/latest/modules/twilio-sms). |
|||
|
|||
@ -0,0 +1,824 @@ |
|||
# Quick Start |
|||
|
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC", "Blazor", "BlazorServer", "NG"], |
|||
"DB": ["EF", "Mongo"] |
|||
} |
|||
```` |
|||
|
|||
This is a single-part, quick-start tutorial to build a simple todo application with the ABP Framework. Here, a screenshot from the final application: |
|||
|
|||
 |
|||
|
|||
You can find source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp). |
|||
|
|||
## Pre-Requirements |
|||
|
|||
* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 5.0+](https://dotnet.microsoft.com/download/dotnet) development. |
|||
|
|||
{{if DB=="Mongo"}} |
|||
|
|||
* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/) |
|||
|
|||
{{end}} |
|||
|
|||
{{if UI=="NG"}} |
|||
|
|||
* [Node v14.x](https://nodejs.org/) |
|||
|
|||
{{end}} |
|||
|
|||
## Creating a New Solution |
|||
|
|||
We will use the [ABP CLI](../../CLI.md) to create new solutions with the ABP Framework. You can run the following command in a command-line terminal to install it: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
Then create an empty folder, open a command-line terminal and execute the following command in the terminal: |
|||
|
|||
````bash |
|||
abp new TodoApp{{if UI=="Blazor"}} -u blazor{{else if UI=="BlazorServer"}} -u blazor-server{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}} |
|||
```` |
|||
|
|||
{{if UI=="NG"}} |
|||
|
|||
This will create a new solution, named *TodoApp* with `angular` and `aspnet-core` folders. Once the solution is ready, open the ASP.NET Core solution in your favorite IDE. |
|||
|
|||
{{else}} |
|||
|
|||
This will create a new solution, named *TodoApp*. Once the solution is ready, open it in your favorite IDE. |
|||
|
|||
{{end}} |
|||
|
|||
### Create the Database |
|||
|
|||
If you are using Visual Studio, right click to the `TodoApp.DbMigrator` project, select *Set as StartUp Project*, then hit *Ctrl+F5* to run it without debugging. It will create the initial database and seed the initial data. |
|||
|
|||
{{if DB=="EF"}} |
|||
|
|||
> Some IDEs (e.g. Rider) may have problems for the first run since *DbMigrator* adds the initial migration and re-compiles the project. In this case, open a command-line terminal in the folder of the `.DbMigrator` project and execute the `dotnet run` command. |
|||
|
|||
{{end}} |
|||
|
|||
### Run the Application |
|||
|
|||
{{if UI=="MVC" || UI=="BlazorServer"}} |
|||
|
|||
It is good to run the application before starting the development. Ensure the {{if UI=="BlazorServer"}}`TodoApp.Blazor`{{else}}`TodoApp.Web`{{end}} project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the initial UI: |
|||
|
|||
{{else if UI=="Blazor"}} |
|||
|
|||
It is good to run the application before starting the development. The solution has two main applications; |
|||
|
|||
* `TodoApp.HttpApi.Host` host the server-side HTTP API. |
|||
* `TodoApp.Blazor` is the client-side Blazor WebAssembly application. |
|||
|
|||
Ensure the `TodoApp.HttpApi.Host` project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the server-side HTTP API on the [Swagger UI](https://swagger.io/tools/swagger-ui/): |
|||
|
|||
 |
|||
|
|||
You can explore and test your HTTP API with this UI. Now, we can set the `TodoApp.Blazor` as the startup project and run it to open the actual Blazor application UI: |
|||
|
|||
{{else if UI=="NG"}} |
|||
|
|||
It is good to run the application before starting the development. The solution has two main applications; |
|||
|
|||
* `TodoApp.HttpApi.Host` (in the .NET solution) host the server-side HTTP API. |
|||
* `angular` folder contains the Angular application. |
|||
|
|||
Ensure the `TodoApp.HttpApi.Host` project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the server-side HTTP API on the [Swagger UI](https://swagger.io/tools/swagger-ui/): |
|||
|
|||
 |
|||
|
|||
You can explore and test your HTTP API with this UI. If that works, we can run the Angular client application. |
|||
|
|||
First, run the following command to restore the NPM packages; |
|||
|
|||
````bash |
|||
npm install |
|||
```` |
|||
|
|||
It will take some time to install all the packages. Then you can run the application using the following command: |
|||
|
|||
````bash |
|||
npm start |
|||
```` |
|||
|
|||
This command takes time, but eventually runs and opens the application in your default browser: |
|||
|
|||
{{end}} |
|||
|
|||
 |
|||
|
|||
You can click to the *Login* button, use `admin` as the username and `1q2w3E*` as the password to login to the application. |
|||
|
|||
All ready. We can start the coding! |
|||
|
|||
## Domain Layer |
|||
|
|||
This application has a single [entity](../../Entities.md) and we are starting by creating it. Create a new `TodoItem` class inside the *TodoApp.Domain* project: |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace TodoApp |
|||
{ |
|||
public class TodoItem : BasicAggregateRoot<Guid> |
|||
{ |
|||
public string Text { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`BasicAggregateRoot` is one the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. |
|||
|
|||
## Database Integration |
|||
|
|||
{{if DB=="EF"}} |
|||
|
|||
Next step is to setup the [Entity Framework Core](../../Entity-Framework-Core.md) configuration. |
|||
|
|||
### Mapping Configuration |
|||
|
|||
Open the `TodoAppDbContext` class in the `EntityFrameworkCore` folder of the *TodoApp.EntityFrameworkCore* project and add a new `DbSet` property to this class: |
|||
|
|||
````csharp |
|||
public DbSet<TodoItem> TodoItems { get; set; } |
|||
```` |
|||
|
|||
Then open the `TodoAppDbContextModelCreatingExtensions` class in the same folder and add a mapping configuration for the `TodoItem` class as shown below: |
|||
|
|||
````csharp |
|||
public static void ConfigureTodoApp(this ModelBuilder builder) |
|||
{ |
|||
Check.NotNull(builder, nameof(builder)); |
|||
|
|||
builder.Entity<TodoItem>(b => |
|||
{ |
|||
b.ToTable("TodoItems"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
We've mapped `TodoItem` entity to a `TodoItems` table in the database. |
|||
|
|||
### Code First Migrations |
|||
|
|||
The startup solution is configured to use Entity Framework Core [Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. |
|||
|
|||
Open a command-line terminal in the directory of the *TodoApp.EntityFrameworkCore.DbMigrations* project and type the following command: |
|||
|
|||
````bash |
|||
dotnet ef migrations add Added_TodoItem |
|||
```` |
|||
|
|||
This will add a new migration class to the project: |
|||
|
|||
 |
|||
|
|||
You can apply changes to the database using the following command, in the same command-line terminal: |
|||
|
|||
````bash |
|||
dotnet ef database update |
|||
```` |
|||
|
|||
> If you are using Visual Studio, you may want to use `Add-Migration Added_TodoItem` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`TodoApp.Web`{{else if UI=="BlazorServer"}}`TodoApp.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`TodoApp.HttpApi.Host`{{end}} is the startup project and `TodoApp.EntityFrameworkCore.DbMigrations` is the *Default Project* in PMC. |
|||
|
|||
{{else if DB=="Mongo"}} |
|||
|
|||
Next step is to setup the [MongoDB](../../MongoDB.md) configuration. Open the `TodoAppMongoDbContext` class in the `MongoDb` folder of the *TodoApp.MongoDB* project and make the following changes; |
|||
|
|||
1. Add a new property to the class: |
|||
|
|||
````csharp |
|||
public IMongoCollection<TodoItem> TodoItems => Collection<TodoItem>(); |
|||
```` |
|||
|
|||
2. Add the following code inside the `CreateModel` method: |
|||
|
|||
````csharp |
|||
modelBuilder.Entity<TodoItem>(b => |
|||
{ |
|||
b.CollectionName = "TodoItems"; |
|||
}); |
|||
```` |
|||
|
|||
{{end}} |
|||
|
|||
Now, we can use ABP repositories to save and retrieve todo items, as we'll do in the next section. |
|||
|
|||
## Application Layer |
|||
|
|||
An [Application Service](../../Application-Services.md) is used to perform use cases of the application. We need to perform the following use cases; |
|||
|
|||
* Get the list of todo items |
|||
* Create a new todo item |
|||
* Delete an existing todo item |
|||
|
|||
### Application Service Interface |
|||
|
|||
We can start by defining an interface for the application service. Create a new `ITodoAppService` interface in the *TodoApp.Application.Contracts* project, as shown below: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace TodoApp |
|||
{ |
|||
public interface ITodoAppService : IApplicationService |
|||
{ |
|||
Task<List<TodoItemDto>> GetListAsync(); |
|||
Task<TodoItemDto> CreateAsync(string text); |
|||
Task DeleteAsync(Guid id); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Data Transfer Object |
|||
|
|||
`GetListAsync` and `CreateAsync` methods return `TodoItemDto`. Applications Services typically gets and returns DTOs ([Data Transfer Objects](../../Data-Transfer-Objects.md)) instead of entities. So, we should define the DTO class here. Create a new `TodoItemDto` class inside the *TodoApp.Application.Contracts* project: |
|||
|
|||
````csharp |
|||
using System; |
|||
|
|||
namespace TodoApp |
|||
{ |
|||
public class TodoItemDto |
|||
{ |
|||
public Guid Id { get; set; } |
|||
public string Text { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This is a very simple DTO class that matches to our `TodoItem` entity. We are ready to implement the `ITodoAppService`. |
|||
|
|||
### Application Service Implementation |
|||
|
|||
Create a `TodoAppService` class inside the *TodoApp.Application* project, as shown below: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace TodoApp |
|||
{ |
|||
public class TodoAppService : ApplicationService, ITodoAppService |
|||
{ |
|||
private readonly IRepository<TodoItem, Guid> _todoItemRepository; |
|||
|
|||
public TodoAppService(IRepository<TodoItem, Guid> todoItemRepository) |
|||
{ |
|||
_todoItemRepository = todoItemRepository; |
|||
} |
|||
|
|||
// TODO: Implement the methods here... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This class inherits from the `ApplicationService` class of the ABP Framework and implements the `ITodoAppService` that was defined before. ABP provides default generic [repositories](../../Repositories.md) for the entities. We can use them to perform the fundamental database operations. This class [injects](../../Dependency-Injection.md) `IRepository<TodoItem, Guid>`, which is the default repository for the `TodoItem` entity. We will use it to implement the use cases described before. |
|||
|
|||
#### Getting Todo Items |
|||
|
|||
Let's start by implementing the `GetListAsync` method: |
|||
|
|||
````csharp |
|||
public async Task<List<TodoItemDto>> GetListAsync() |
|||
{ |
|||
var items = await _todoItemRepository.GetListAsync(); |
|||
return items |
|||
.Select(item => new TodoItemDto |
|||
{ |
|||
Id = item.Id, |
|||
Text = item.Text |
|||
}).ToList(); |
|||
} |
|||
```` |
|||
|
|||
We are simply getting the complete `TodoItem` list from database, mapping them to `TodoItemDto` objects and returning as the result. |
|||
|
|||
#### Creating a New Todo Item |
|||
|
|||
Next method is `CreateAsync` and we can implement it as shown below: |
|||
|
|||
````csharp |
|||
public async Task<TodoItemDto> CreateAsync(string text) |
|||
{ |
|||
var todoItem = await _todoItemRepository.InsertAsync( |
|||
new TodoItem {Text = text} |
|||
); |
|||
|
|||
return new TodoItemDto |
|||
{ |
|||
Id = todoItem.Id, |
|||
Text = todoItem.Text |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
Repository's `InsertAsync` method inserts the given `TodoItem` to database and returns the same `TodoItem` object. It also sets the `Id`, so we can use it on the returning object. We are simply returning a `TodoItemDto` by creating from the new `TodoItem` entity. |
|||
|
|||
#### Deleting a Todo Item |
|||
|
|||
Finally, we can implement the `DeleteAsync` as the following code block: |
|||
|
|||
````csharp |
|||
public async Task DeleteAsync(Guid id) |
|||
{ |
|||
await _todoItemRepository.DeleteAsync(id); |
|||
} |
|||
```` |
|||
|
|||
The application service is ready to be used from the UI layer. |
|||
|
|||
## User Interface Layer |
|||
|
|||
It is time to show the todo items on the UI! Before starting to write the code, it would be good to remember what we are trying to build. Here, a sample screenshot from the final UI: |
|||
|
|||
 |
|||
|
|||
> **We will keep the UI side minimal for this tutorial to make the tutorial simple and focused. See the [web application development tutorial](../Part-1.md) to build real-life pages with all aspects.** |
|||
|
|||
{{if UI=="MVC"}} |
|||
|
|||
### Index.cshtml.cs |
|||
|
|||
Open the `Index.cshtml.cs` file in the `Pages` folder of the *TodoApp.Web* project and replace the content with the following code block: |
|||
|
|||
````csharp |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace TodoApp.Web.Pages |
|||
{ |
|||
public class IndexModel : TodoAppPageModel |
|||
{ |
|||
public List<TodoItemDto> TodoItems { get; set; } |
|||
|
|||
private readonly ITodoAppService _todoAppService; |
|||
|
|||
public IndexModel(ITodoAppService todoAppService) |
|||
{ |
|||
_todoAppService = todoAppService; |
|||
} |
|||
|
|||
public async Task OnGetAsync() |
|||
{ |
|||
TodoItems = await _todoAppService.GetListAsync(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This class uses the `ITodoAppService` to get the list of todo items and assign the the `TodoItems` property. We will use it to render the todo items on the razor page. |
|||
|
|||
### Index.cshtml |
|||
|
|||
Open the `Index.cshtml` file in the `Pages` folder of the *TodoApp.Web* project and replace with the following content: |
|||
|
|||
````xml |
|||
@page |
|||
@model TodoApp.Web.Pages.IndexModel |
|||
@section styles { |
|||
<abp-style src="/Pages/Index.css" /> |
|||
} |
|||
@section scripts { |
|||
<abp-script src="/Pages/Index.js" /> |
|||
} |
|||
<div class="container"> |
|||
<abp-card> |
|||
<abp-card-header> |
|||
<abp-card-title> |
|||
TODO LIST |
|||
</abp-card-title> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<!-- FORM FOR NEW TODO ITEMS --> |
|||
<form id="NewItemForm" class="form-inline"> |
|||
<input id="NewItemText" |
|||
type="text" |
|||
class="form-control mr-2" |
|||
placeholder="enter text..."> |
|||
<button type="submit" class="btn btn-primary">Submit</button> |
|||
</form> |
|||
|
|||
<!-- TODO ITEMS LIST --> |
|||
<ul id="TodoList"> |
|||
@foreach (var todoItem in Model.TodoItems) |
|||
{ |
|||
<li data-id="@todoItem.Id"> |
|||
<i class="fa fa-trash-o"></i> @todoItem.Text |
|||
</li> |
|||
} |
|||
</ul> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
</div> |
|||
```` |
|||
|
|||
We are using ABP's [card tag helper](../../UI/AspNetCore/Tag-Helpers/Cards.md) to create a simple card view. You could directly use the standard bootstrap HTML structure, however the ABP [tag helpers]() make it much easier and type safe. |
|||
|
|||
This page imports a CSS and a JavaScript file, so we should also create them. |
|||
|
|||
### Index.js |
|||
|
|||
Open the `Index.js` file in the `Pages` folder of the *TodoApp.Web* project and replace with the following content: |
|||
|
|||
````js |
|||
$(function () { |
|||
|
|||
// DELETING ITEMS ///////////////////////////////////////// |
|||
$('#TodoList').on('click', 'li i', function(){ |
|||
var $li = $(this).parent(); |
|||
var id = $li.attr('data-id'); |
|||
|
|||
todoApp.todo.delete(id).then(function(){ |
|||
$li.remove(); |
|||
abp.notify.info('Deleted the todo item.'); |
|||
}); |
|||
}); |
|||
|
|||
// CREATING NEW ITEMS ///////////////////////////////////// |
|||
$('#NewItemForm').submit(function(e){ |
|||
e.preventDefault(); |
|||
|
|||
var todoText = $('#NewItemText').val(); |
|||
todoApp.todo.create(todoText).then(function(result){ |
|||
$('<li data-id="' + result.id + '">') |
|||
.html('<i class="fa fa-trash-o"></i> ' + result.text) |
|||
.appendTo($('#TodoList')); |
|||
$('#NewItemText').val(''); |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
In the first part, we are registering to click events of the trash icons near to the todo items, deleting the related item on the server and showing a notification on the UI. Also, we are removing the deleted item from DOM, so we don't need to refresh the page. |
|||
|
|||
In the second part, we are creating a new todo item on the server. If it succeed, we are then manipulating DOM to insert a new `<li>` element to the todo list. In this way, no need to refresh the whole page after creating a new todo item. |
|||
|
|||
The interesting part here is how we communicate with the server. See the *Dynamic JavaScript Proxies & Auto API Controllers* section to understand how it works. But now, let's continue and complete the application. |
|||
|
|||
### Index.css |
|||
|
|||
As the final touch, open the `Index.css` file in the `Pages` folder of the *TodoApp.Web* project and replace with the following content: |
|||
|
|||
````css |
|||
#TodoList{ |
|||
list-style: none; |
|||
margin: 0; |
|||
padding: 0; |
|||
} |
|||
|
|||
#TodoList li { |
|||
padding: 5px; |
|||
margin: 5px 0px; |
|||
border: 1px solid #cccccc; |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
#TodoList li i |
|||
{ |
|||
opacity: 0.5; |
|||
} |
|||
|
|||
#TodoList li i:hover |
|||
{ |
|||
opacity: 1; |
|||
color: #ff0000; |
|||
cursor: pointer; |
|||
} |
|||
```` |
|||
|
|||
This is a simple styling for the todo page. We believe that you can do much better :) |
|||
|
|||
Now, you can run the application again to see the result. |
|||
|
|||
### Dynamic JavaScript Proxies & Auto API Controllers |
|||
|
|||
In the `Index.js` file, we've used `todoApp.todo.delete(...)` and `todoApp.todo.create(...)` functions to communicate with the server. These functions are dynamically created by the ABP Framework, thanks to the [Dynamic JavaScript Client Proxy](../../UI/AspNetCore/Dynamic-JavaScript-Proxies.md) system. They perform HTTP API calls to the server and return a promise, so you can register a callback to the `then` function as we've done above. |
|||
|
|||
However, you may ask that we haven't created any API Controller, so how server handles these requests? This question brings us the [Auto API Controller](../../API/Auto-API-Controllers.md) feature of the ABP Framework. It automatically converts the application services to API Controllers by conventions. |
|||
|
|||
If you open the [Swagger UI](https://swagger.io/tools/swagger-ui/) by entering the `/swagger` URL in your application, you can see the Todo API: |
|||
|
|||
 |
|||
|
|||
{{else if UI=="Blazor" || UI=="BlazorServer"}} |
|||
|
|||
### Index.razor.cs |
|||
|
|||
Open the `Index.razor.cs` file in the `Pages` folder of the *TodoApp.Blazor* project and replace the content with the following code block: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Components; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace TodoApp.Blazor.Pages |
|||
{ |
|||
public partial class Index |
|||
{ |
|||
[Inject] |
|||
private ITodoAppService TodoAppService { get; set; } |
|||
|
|||
private List<TodoItemDto> TodoItems { get; set; } = new List<TodoItemDto>(); |
|||
private string NewTodoText { get; set; } |
|||
|
|||
protected async override Task OnInitializedAsync() |
|||
{ |
|||
TodoItems = await TodoAppService.GetListAsync(); |
|||
} |
|||
|
|||
private async Task Create() |
|||
{ |
|||
var result = await TodoAppService.CreateAsync(NewTodoText); |
|||
TodoItems.Add(result); |
|||
NewTodoText = null; |
|||
} |
|||
|
|||
private async Task Delete(TodoItemDto todoItem) |
|||
{ |
|||
await TodoAppService.DeleteAsync(todoItem.Id); |
|||
await Notify.Info("Deleted the todo item."); |
|||
TodoItems.Remove(todoItem); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This class uses the `ITodoAppService` to perform operations for the todo items. It manipulates the `TodoItems` list after create and delete operations. In this way, we don't need to refresh the whole todo list from the server. |
|||
|
|||
{{if UI=="Blazor"}} |
|||
|
|||
See the *Dynamic C# Proxies & Auto API Controllers* section below to learn how we could inject and use the application service interface from the Blazor application which is running on the browser! But now, let's continue and complete the application. |
|||
|
|||
{{end # Blazor}} |
|||
|
|||
### Index.razor |
|||
|
|||
Open the `Index.razor` file in the `Pages` folder of the *TodoApp.Blazor* project and replace the content with the following code block: |
|||
|
|||
````xml |
|||
@page "/" |
|||
@inherits TodoAppComponentBase |
|||
<div class="container"> |
|||
<Card> |
|||
<CardHeader> |
|||
<CardTitle> |
|||
TODO LIST |
|||
</CardTitle> |
|||
</CardHeader> |
|||
<CardBody> |
|||
<!-- FORM FOR NEW TODO ITEMS --> |
|||
<form id="NewItemForm" |
|||
@onsubmit:preventDefault |
|||
@onsubmit="() => Create()" |
|||
class="form-inline"> |
|||
<input type="text" |
|||
@bind-value="@NewTodoText" |
|||
class="form-control mr-2" |
|||
placeholder="enter text..."> |
|||
<button type="submit" class="btn btn-primary">Submit</button> |
|||
</form> |
|||
|
|||
<!-- TODO ITEMS LIST --> |
|||
<ul id="TodoList"> |
|||
@foreach (var todoItem in TodoItems) |
|||
{ |
|||
<li data-id="@todoItem.Id"> |
|||
<i class="far fa-trash-alt" |
|||
@onclick="() => Delete(todoItem)" |
|||
></i> @todoItem.Text |
|||
</li> |
|||
} |
|||
</ul> |
|||
</CardBody> |
|||
</Card> |
|||
</div> |
|||
```` |
|||
|
|||
### Index.razor.css |
|||
|
|||
As the final touch, open the `Index.razor.css` file in the `Pages` folder of the *TodoApp.Web* project and replace with the following content: |
|||
|
|||
````css |
|||
#TodoList{ |
|||
list-style: none; |
|||
margin: 0; |
|||
padding: 0; |
|||
} |
|||
|
|||
#TodoList li { |
|||
padding: 5px; |
|||
margin: 5px 0px; |
|||
border: 1px solid #cccccc; |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
#TodoList li i |
|||
{ |
|||
opacity: 0.5; |
|||
} |
|||
|
|||
#TodoList li i:hover |
|||
{ |
|||
opacity: 1; |
|||
color: #ff0000; |
|||
cursor: pointer; |
|||
} |
|||
```` |
|||
|
|||
This is a simple styling for the todo page. We believe that you can do much better :) |
|||
|
|||
Now, you can run the application again to see the result. |
|||
|
|||
{{if UI=="Blazor"}} |
|||
|
|||
### Dynamic C# Proxies & Auto API Controllers |
|||
|
|||
In the `Index.razor.cs` file, we've injected (with the `[Inject]` attribute) and used the `ITodoAppService` just like using a local service. Remember that the Blazor application is running on the browser while the implementation of this application service is running on the server. |
|||
|
|||
The magic is done by the ABP Framework's [Dynamic C# Client Proxy](../../API/Dynamic-CSharp-API-Clients.md) system. It uses the standard `HttpClient` and performs HTTP API requests to the remote server. It also handles all the standard tasks for us, including authorization, JSON serialization and exception handling. |
|||
|
|||
However, you may ask that we haven't created any API Controller, so how server handles these requests? This question brings us the [Auto API Controller](../../API/Auto-API-Controllers.md) feature of the ABP Framework. It automatically converts the application services to API Controllers by conventions. |
|||
|
|||
If you run the `TodoApp.HttpApi.Host` application, you can see the Todo API: |
|||
|
|||
 |
|||
|
|||
{{end # Blazor}} |
|||
|
|||
{{else if UI=="NG"}} |
|||
|
|||
### Service Proxy Generation |
|||
|
|||
ABP provides a handy feature to automatically create client-side services to easily consume HTTP APIs provided by the server. |
|||
|
|||
You first need to run the `TodoApp.HttpApi.Host` project since the proxy generator reads API definitions from the server application. |
|||
|
|||
> **Warning**: There is a problem with IIS Express; it doesn't allow to connect to the application from another process. If you are using Visual Studio, select the `TodoApp.HttpApi.Host` instead of IIS Express in the run button drop-down list, as shown in the figure below: |
|||
|
|||
 |
|||
|
|||
Once you run the `TodoApp.HttpApi.Host` project, open a command-line terminal in the `angular` folder and type the following command: |
|||
|
|||
````bash |
|||
abp generate-proxy |
|||
```` |
|||
|
|||
If everything goes well, it should generate an output like shown below: |
|||
|
|||
````bash |
|||
CREATE src/app/proxy/generate-proxy.json (170978 bytes) |
|||
CREATE src/app/proxy/README.md (1000 bytes) |
|||
CREATE src/app/proxy/todo.service.ts (794 bytes) |
|||
CREATE src/app/proxy/models.ts (66 bytes) |
|||
CREATE src/app/proxy/index.ts (58 bytes) |
|||
```` |
|||
|
|||
We can then use the `todoService` to use the server-side HTTP APIs, as we'll do in the next section. |
|||
|
|||
### home.component.ts |
|||
|
|||
Open the `/angular/src/app/home/home.component.ts` file and replace its content with the following code block: |
|||
|
|||
````js |
|||
import { ToasterService } from '@abp/ng.theme.shared'; |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { TodoItemDto, TodoService } from '@proxy'; |
|||
|
|||
@Component({ |
|||
selector: 'app-home', |
|||
templateUrl: './home.component.html', |
|||
styleUrls: ['./home.component.scss'] |
|||
}) |
|||
export class HomeComponent implements OnInit { |
|||
|
|||
todoItems: TodoItemDto[]; |
|||
newTodoText: string; |
|||
|
|||
constructor( |
|||
private todoService: TodoService, |
|||
private toasterService: ToasterService) |
|||
{ } |
|||
|
|||
ngOnInit(): void { |
|||
this.todoService.getList().subscribe(response => { |
|||
this.todoItems = response; |
|||
}); |
|||
} |
|||
|
|||
create(): void{ |
|||
this.todoService.create(this.newTodoText).subscribe((result) => { |
|||
this.todoItems = this.todoItems.concat(result); |
|||
this.newTodoText = null; |
|||
}); |
|||
} |
|||
|
|||
delete(id: string): void { |
|||
this.todoService.delete(id).subscribe(() => { |
|||
this.todoItems = this.todoItems.filter(item => item.id !== id); |
|||
this.toasterService.info('Deleted the todo item.'); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
We've used the `todoService` to get the list of todo items and assigned the returning value to the `todoItems` array. We've also added `create` and `delete` methods. These methods will be used in the view side. |
|||
|
|||
### home.component.html |
|||
|
|||
Open the `/angular/src/app/home/home.component.html` file and replace its content with the following code block: |
|||
|
|||
````html |
|||
<div class="container"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<div class="card-title">TODO LIST</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<!-- FORM FOR NEW TODO ITEMS --> |
|||
<form class="form-inline" (ngSubmit)="create()"> |
|||
<input |
|||
name="NewTodoText" |
|||
type="text" |
|||
[(ngModel)]="newTodoText" |
|||
class="form-control mr-2" |
|||
placeholder="enter text..." |
|||
/> |
|||
<button type="submit" class="btn btn-primary">Submit</button> |
|||
</form> |
|||
|
|||
<!-- TODO ITEMS LIST --> |
|||
<ul id="TodoList"> |
|||
<li *ngFor="let todoItem of todoItems"> |
|||
<i class="fa fa-trash-o" (click)="delete(todoItem.id)"></i> {%{{{ todoItem.text }}}%} |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
```` |
|||
|
|||
### home.component.scss |
|||
|
|||
As the final touch, open the `/angular/src/app/home/home.component.scss` file and replace its content with the following code block: |
|||
|
|||
````css |
|||
#TodoList{ |
|||
list-style: none; |
|||
margin: 0; |
|||
padding: 0; |
|||
} |
|||
|
|||
#TodoList li { |
|||
padding: 5px; |
|||
margin: 5px 0px; |
|||
border: 1px solid #cccccc; |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
#TodoList li i |
|||
{ |
|||
opacity: 0.5; |
|||
} |
|||
|
|||
#TodoList li i:hover |
|||
{ |
|||
opacity: 1; |
|||
color: #ff0000; |
|||
cursor: pointer; |
|||
} |
|||
```` |
|||
|
|||
This is a simple styling for the todo page. We believe that you can do much better :) |
|||
|
|||
Now, you can run the application again to see the result. |
|||
|
|||
{{end}} |
|||
|
|||
## Conclusion |
|||
|
|||
In this tutorial, we've build a very simple application to warm up to the ABP Framework. If you are looking to build a serious application, please check the [web application development tutorial](../Part-1.md) which covers all the aspects of a real-life web application development. |
|||
|
|||
## Source Code |
|||
|
|||
You can find source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp). |
|||
|
|||
## See Also |
|||
|
|||
* [Web Application Development Tutorial](../Part-1.md) |
|||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,218 @@ |
|||
# Page Component |
|||
|
|||
ABP provides a component that wraps your content with some built-in components to reduce the amount of code you need to write. |
|||
|
|||
If the template of a component looks as follows, you can utilize the `abp-page` component. |
|||
|
|||
Let's look at the following example without `abp-page` component. |
|||
|
|||
`dashboard.component.ts` |
|||
|
|||
```html |
|||
<div class="row entry-row"> |
|||
<div class="col-auto"> |
|||
<h1 class="content-header-title">{{ '::Dashboard' | abpLocalization }}</h1> |
|||
</div> |
|||
<div id="breadcrumb" class="col-lg-auto pl-lg-0"> |
|||
<abp-breadcrumb></abp-breadcrumb> |
|||
</div> |
|||
<div class="col"> |
|||
<abp-page-toolbar [record]="data"></abp-page-toolbar> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="dashboard-id"> |
|||
<!-- dashboard content here --> |
|||
</div> |
|||
``` |
|||
|
|||
## Page Parts |
|||
|
|||
PageComponent divides the template shown above into three parts, `title`, `breadcrumb`, `toolbar`. Each can be configured separately. There, also, is an enum exported from the package that describes each part. |
|||
|
|||
```javascript |
|||
export enum PageParts { |
|||
title = 'PageTitleContainerComponent', |
|||
breadcrumb = 'PageBreadcrumbContainerComponent', |
|||
toolbar = 'PageToolbarContainerComponent', |
|||
} |
|||
|
|||
// You can import this enum from -> import { PageParts } from '@abp/ng.components/page'; |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
Firstly, you need to import `PageModule` from `@abp/ng.components/page` as follows: |
|||
|
|||
`dashboard.module.ts` |
|||
|
|||
```javascript |
|||
import { PageModule } from '@abp/ng.components/page'; |
|||
import { DashboardComponent } from './dashboard.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [DashboardComponent], |
|||
imports: [PageModule] |
|||
}) |
|||
export class DashboardModule {} |
|||
``` |
|||
|
|||
And change the template of `dashboard.component.ts` to the following: |
|||
|
|||
```html |
|||
<abp-page [title]="'::Dashboard' | abpLocalization" [toolbar]="data"> |
|||
<div id="dashboard-id"> |
|||
<!-- .... --> |
|||
</div> |
|||
</abp-page> |
|||
``` |
|||
|
|||
## Inputs |
|||
|
|||
* title: `string`: Will be be rendered within `h1.content-header-title`. If not provided, the parent `div` will not be rendered |
|||
* breadcrumb: `boolean`: Determines whether to render `abp-breadcrumb`. Default is `true`. |
|||
* toolbar: `any`: Will be passed into `abp-page-toolbar` component through `record` input. If your page does not contain `abp-page-toolbar`, you can simply omit this field. |
|||
|
|||
## Overriding template |
|||
|
|||
If you need to replace the template of any part, you can use the following sub-components. |
|||
|
|||
```html |
|||
<abp-page> |
|||
<abp-page-title-container> |
|||
<div class="col"> |
|||
<h2>Custom Title</h2> |
|||
</div> |
|||
</abp-page-title-container> |
|||
|
|||
<abp-page-breacrumb-container> |
|||
<div class="col"> |
|||
<my-breadcrumb></my-breadcrumb> |
|||
</div> |
|||
</abp-page-breacrumb-container> |
|||
|
|||
<abp-page-toolbar-container> |
|||
<div class="col"> |
|||
<!-- ... --> |
|||
</div> |
|||
</abp-page-toolbar-container> |
|||
</abp-page> |
|||
``` |
|||
|
|||
You do not have to provide them all. You can just use which one you need to replace. These components have priority over the inputs declared above. If you use these components, you can omit the inputs. |
|||
|
|||
## PagePartDirective |
|||
|
|||
`PageModule` provides a structural directive that is used internally within `PageComponent` and can also be used externally. |
|||
|
|||
`PageComponent` employs this directive internally as follows: |
|||
|
|||
```html |
|||
<div class="col-lg-auto pl-lg-0" *abpPagePart="pageParts.breadcrumb"> |
|||
<abp-breadcrumb></abp-breadcrumb> |
|||
</div> |
|||
``` |
|||
|
|||
It also can take a context input as follows: |
|||
|
|||
```html |
|||
<div class="col" *abpPagePart="pageParts.toolbar; context: toolbarData"> |
|||
<abp-page-toolbar [record]="toolbarData"></abp-page-toolbar> |
|||
</div> |
|||
``` |
|||
|
|||
Its render strategy can be provided through Angular's Dependency Injection system. |
|||
|
|||
It expects a service through the `PAGE_RENDER_STRATEGY` injection token that implements the following interface. |
|||
|
|||
```javascript |
|||
interface PageRenderStrategy { |
|||
shouldRender(type?: string): boolean | Observable<boolean>; |
|||
onInit?(type?: string, injector?: Injector, context?: any): void; |
|||
onDestroy?(type?: string, injector?: Injector, context?: any): void; |
|||
onContextUpdate?(change?: SimpleChange): void; |
|||
} |
|||
``` |
|||
|
|||
* `shouldRender` (required): It takes a string input named `type` and expects a `boolean` or `Observable<boolean>` in return. |
|||
* `onInit` (optional): Will be called when the directive is initiated. Three inputs will be passed into this method. |
|||
* `type`: type of the page part |
|||
* `injector`: injector of the directive which could be used to retrieve anything from directive's DI tree. |
|||
* `context`: whatever context is available at the initialization phase. |
|||
* `onDestroy` (optional): Will be called when the directive is destroyed. The parameters are the same with `onInit` |
|||
* `onContextUpdate` (optional): Will be called when the context is updated. |
|||
* `change`: changes of the `context` will be passed through this method. |
|||
|
|||
Let's see everything in action. |
|||
|
|||
```javascript |
|||
import { |
|||
PageModule, |
|||
PageRenderStrategy, |
|||
PageParts, |
|||
PAGE_RENDER_STRATEGY |
|||
} from '@abp/ng.components/page'; |
|||
|
|||
@Injectable() |
|||
export class MyPageRenderStrategy implements PageRenderStrategy { |
|||
shouldRender(type: string) { |
|||
// meaning everything but breadcrumb and custom-part will be rendered |
|||
return type !== PageParts.breadcrumb && type !== 'custom-part'; |
|||
} |
|||
|
|||
/** |
|||
* shouldRender can also return an Observable<boolean> which means |
|||
* an async service can be used within. |
|||
|
|||
constructor(private service: SomeAsyncService) {} |
|||
|
|||
shouldRender(type: string) { |
|||
return this.service.checkTypeAsync(type).pipe(map(val => val.isTrue())); |
|||
} |
|||
*/ |
|||
|
|||
onInit(type: string, injector: Injector, context: any) { |
|||
// this method will be called in ngOnInit of the directive |
|||
} |
|||
|
|||
onDestroy(type: string, injector: Injector, context: any) { |
|||
// this method will be called in ngOnDestroy of the directive |
|||
} |
|||
|
|||
onContextUpdate?(change?: SimpleChange) { |
|||
// this method will be called everytime context is updated within the directive |
|||
} |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'app-dashboard', |
|||
template: ` |
|||
<abp-page [title]="'::Dashboard' | abpLocalization"> |
|||
<abp-page-toolbar-container> |
|||
<button>New Dashboard</button> |
|||
</abp-page-toolbar-container> |
|||
|
|||
<div class="dashboard-content"> |
|||
<h3 *abpPagePart="'custom-part'"> Inner Title </h3> |
|||
</div> |
|||
</abp-page> |
|||
` |
|||
}) |
|||
export class DashboardComponent {} |
|||
|
|||
@NgModule({ |
|||
imports: [PageModule], |
|||
declarations: [DashboardComponent], |
|||
providers: [ |
|||
{ |
|||
provide: PAGE_RENDER_STRATEGY, |
|||
useClass: MyPageRenderStrategy, |
|||
} |
|||
] |
|||
}) |
|||
export class DashboardModule {} |
|||
``` |
|||
|
|||
## See Also |
|||
|
|||
- [Page Toolbar Extensions for Angular UI](./Page-Page-Toolbar-Extensions.md) |
|||
@ -1,3 +0,0 @@ |
|||
# ASP.NET Core MVC / Razor Pages UI: Dynamic JavaScript Client Proxies |
|||
|
|||
TODO |
|||
@ -0,0 +1,128 @@ |
|||
# Data Table Column Extensions for Blazor UI |
|||
|
|||
Data table column extension system allows you to add a **new table column** on the user interface. The example below adds a new column with the "Email Confirmed" title: |
|||
|
|||
 |
|||
|
|||
You can use the standard column options to fine control the table column. |
|||
|
|||
> Note that this is a low level API to find control the table column. If you want to show an extension property on the table, see the [module entity extension](../../Module-Entity-Extensions.md) document. |
|||
|
|||
## How to Set Up |
|||
|
|||
### Create a C# File |
|||
|
|||
First, add a new C# file to your solution. We added inside the `/Pages/Identity/` folder of the `.Blazor` project: |
|||
|
|||
 |
|||
|
|||
We will use the [component override system](Customization-Overriding-Components.md) in the Blazor. After creating a class inherits from the `UserManagement` component, we will override the `SetTableColumnsAsync` method and add the table column programmatically. |
|||
|
|||
Here, the content of the overridden `SetTableColumnsAsync` method. |
|||
|
|||
```csharp |
|||
protected override async ValueTask SetTableColumnsAsync() |
|||
{ |
|||
await base.SetTableColumnsAsync(); |
|||
var confirmedColumn = new TableColumn |
|||
{ |
|||
Title = "Email Confirmed", |
|||
Data = nameof(IdentityUserDto.EmailConfirmed) |
|||
}; |
|||
TableColumns.Get<UserManagement>().Add(confirmedColumn); |
|||
} |
|||
``` |
|||
Here, the entire content of the file. |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Identity.Blazor.Pages.Identity; |
|||
|
|||
namespace MyCompanyName.MyProjectName.Blazor.Pages.Identity |
|||
{ |
|||
[ExposeServices(typeof(UserManagement))] |
|||
[Dependency(ReplaceServices = true)] |
|||
public class CustomizedUserManagement : UserManagement |
|||
{ |
|||
protected override async ValueTask SetTableColumnsAsync() |
|||
{ |
|||
await base.SetTableColumnsAsync(); |
|||
var confirmedColumn = new TableColumn |
|||
{ |
|||
Title = "Email Confirmed", |
|||
Data = nameof(IdentityUserDto.EmailConfirmed) |
|||
}; |
|||
TableColumns.Get<UserManagement>().Add(confirmedColumn); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Customizing Data Table Columns |
|||
|
|||
This section explains how to customize data table columns using the properties in the `TableColumn` type. |
|||
|
|||
* `Title`: Title of the column. |
|||
* `Data`: Name of the field in the supplied model. |
|||
* `Component`: Type of the component that you want to render. See the "Rendering Custom Components In The Data Table Columns" section for details. |
|||
* `Actions`: Action lists for the column. You can render additional action columns by adding actions to this collection. |
|||
* `ValueConverter`: Simple converter function that is being called before rendering the content. |
|||
* `Sortable`: A boolean field indicating whether the column is sortable or not. |
|||
* `DisplayFormat`: You can specify a custom format for the column. |
|||
* `DisplayFormatProvider` : You can provide a custom `IFormatProvider` for the column. Default value is `CultureInfo.CurrentCulture`. |
|||
|
|||
## Rendering Custom Components In The Data Table Columns |
|||
|
|||
This section explains how to render custom blazor components in data table columns. In this example, we're going to display custom icons instead of text representations of the property. |
|||
|
|||
First of all, create a blazor component. We will name it `CustomTableColumn`. |
|||
|
|||
 |
|||
|
|||
Add an object parameter named `Data`. |
|||
|
|||
```csharp |
|||
public class CustomTableColumn |
|||
{ |
|||
[Parameter] |
|||
public object Data { get; set; } |
|||
} |
|||
``` |
|||
|
|||
Navigate to the razor file and paste the following code. |
|||
|
|||
```csharp |
|||
@using System |
|||
@using Volo.Abp.Identity |
|||
|
|||
@if (Data.As<IdentityUserDto>().EmailConfirmed) |
|||
{ |
|||
<Icon class="text-success" Name="IconName.Check" /> |
|||
} |
|||
else |
|||
{ |
|||
<Icon class="text-danger" Name="IconName.Times" /> |
|||
} |
|||
``` |
|||
|
|||
Navigate back to the `CustomizedUserManagement` class, and use `Component` property to specify the custom blazor component. |
|||
|
|||
```csharp |
|||
protected override async ValueTask SetTableColumnsAsync() |
|||
{ |
|||
await base.SetTableColumnsAsync(); |
|||
var confirmedColumn = new TableColumn |
|||
{ |
|||
Title = "Email Confirmed", |
|||
Component = typeof(CustomTableColumn) |
|||
}; |
|||
TableColumns.Get<UserManagement>().Add(confirmedColumn); |
|||
} |
|||
``` |
|||
|
|||
Run the project and you will see the icons instead of text fields. |
|||
|
|||
 |
|||
@ -0,0 +1,109 @@ |
|||
# Entity Action Extensions for Blazor UI |
|||
|
|||
Entity action extension system allows you to add a **new action** to the action menu for an entity. A **Click Me** action was added to the *User Management* page below: |
|||
|
|||
 |
|||
|
|||
You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can access to the current entity in your code. |
|||
|
|||
## How to Set Up |
|||
|
|||
In this example, we will add a "Click Me!" action and execute a C# code for the user management page of the [Identity Module](../../Modules/Identity.md). |
|||
|
|||
### Create a C# File |
|||
|
|||
First, add a new C# file to your solution. We added inside the `/Pages/Identity/` folder of the `.Blazor` project: |
|||
|
|||
 |
|||
|
|||
We will use the [component override system](Customization-Overriding-Components.md) in the Blazor. After creating a class inherits from the `UserManagement` component, we will override the `SetToolbarItemsAsync` method and add the entity action programmatically. |
|||
|
|||
Here, the content of the overridden `SetToolbarItemsAsync` method. |
|||
|
|||
```csharp |
|||
protected override async ValueTask SetToolbarItemsAsync() |
|||
{ |
|||
await base.SetToolbarItemsAsync(); |
|||
var clickMeAction = new EntityAction() |
|||
{ |
|||
Text = "Click Me!", |
|||
Clicked = (data) => |
|||
{ |
|||
//TODO: Write your custom code |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
}; |
|||
|
|||
EntityActions.Get<UserManagement>().Add(clickMeAction); |
|||
} |
|||
``` |
|||
|
|||
In the `Clicked` property, you can do anything you need. |
|||
|
|||
Here, the entire content of the file. |
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Components.Web.Extensibility.EntityActions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity.Blazor.Pages.Identity; |
|||
|
|||
namespace MyCompanyName.MyProjectName.Blazor.Pages.Identity |
|||
{ |
|||
[ExposeServices(typeof(UserManagement))] |
|||
[Dependency(ReplaceServices = true)] |
|||
public class CustomizedUserManagement : UserManagement |
|||
{ |
|||
protected override async ValueTask SetToolbarItemsAsync() |
|||
{ |
|||
await base.SetToolbarItemsAsync(); |
|||
var clickMeAction = new EntityAction() |
|||
{ |
|||
Text = "Click Me!", |
|||
Clicked = (data) => |
|||
{ |
|||
//TODO: Write your custom code |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
}; |
|||
EntityActions.Get<UserManagement>().Add(clickMeAction); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Customizing Entity Actions |
|||
|
|||
This section explains how to customize entity actions using the properties in the `EntityAction` type. |
|||
|
|||
Here, the list of the properties that you use in the `EntityAction`. |
|||
* `Text` : Entity action text. |
|||
* `Clicked` : Click event handler for the action. You can use the `data` parameter to access the selected item in the `DataGrid`. |
|||
* `Icon` : Icon for the action. |
|||
* `Color` : Color for the action. |
|||
* `Visible`: Visible function to determine the actions' visibility based on the data grid items individually. You can make the action invisible for some data grid items. You can also use the `data` parameter to access the selected item in the `DataGrid`. |
|||
* `Confirmation`: Confirmation message for the action. You can use the `data` parameter to access the selected item in the `DataGrid`. |
|||
|
|||
#### Example |
|||
|
|||
```csharp |
|||
var clickMeAction = new EntityAction() |
|||
{ |
|||
Text = "Click Me!", |
|||
Clicked = (data) => |
|||
{ |
|||
//TODO: Write your custom code |
|||
|
|||
return Task.CompletedTask; |
|||
}, |
|||
Color = Blazorise.Color.Danger, |
|||
Icon = "fas fa-hand-point-right", |
|||
ConfirmationMessage = (data) => "Are you sure you want to click to the action?", |
|||
Visible = (data) => |
|||
{ |
|||
//TODO: Write your custom visibility action |
|||
//var selectedUser = data.As<IdentityUserDto>(); |
|||
} |
|||
}; |
|||
``` |
|||
@ -1,3 +1,90 @@ |
|||
# Blazor UI: Page Header |
|||
|
|||
TODO |
|||
You can use the`PageHeader` component to set the page title, the breadcrumb items and the toolbar items for a page. Before using the `PageHeader` component, you need to add a using statement for the `Volo.Abp.AspNetCore.Components.Web.Theming.Layout` namespace. |
|||
|
|||
Once you add the `PageHeader` component to your page, you can control the related values using the parameters. |
|||
|
|||
## Page Title |
|||
|
|||
You can use the `Title` parameter to control the page header. |
|||
|
|||
```csharp |
|||
<PageHeader Title="Book List"> |
|||
</PageHeader> |
|||
``` |
|||
|
|||
## Breadcrumb |
|||
|
|||
> **The [Basic Theme](Basic-Theme.md) currently doesn't implement the breadcrumbs.** |
|||
|
|||
Breadcrumbs can be added using the `BreadcrumbItems` property. |
|||
|
|||
**Example: Add Language Management to the breadcrumb items.** |
|||
|
|||
Create a collection of `Volo.Abp.BlazoriseUI.BreadcrumbItem` objects and set the collection to the `BreadcrumbItems` parameter. |
|||
|
|||
```csharp |
|||
public partial class Index |
|||
{ |
|||
protected List<BreadcrumbItem> BreadcrumbItems { get; } = new(); |
|||
|
|||
protected override void OnInitialized() |
|||
{ |
|||
BreadcrumbItems.Add(new BreadcrumbItem("Language Management")); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Navigate back to the razor page. |
|||
|
|||
```csharp |
|||
<PageHeader BreadcrumbItems="@BreadcrumbItems" /> |
|||
``` |
|||
|
|||
The theme then renders the breadcrumb. An example render result can be: |
|||
|
|||
 |
|||
|
|||
* The Home icon is rendered by default. Set `BreadcrumbShowHome` to `false` to hide it. |
|||
* Breadcrumb items will be activated based on current navigation. Set `BreadcrumbShowCurrent` to `false` to disable it. |
|||
|
|||
You can add as many items as you need. `BreadcrumbItem` constructor gets three parameters: |
|||
|
|||
* `text`: The text to show for the breadcrumb item. |
|||
* `url` (optional): A URL to navigate to, if the user clicks to the breadcrumb item. |
|||
* `icon` (optional): An icon class (like `fas fa-user-tie` for Font-Awesome) to show with the `text`. |
|||
|
|||
## Page Toolbar |
|||
|
|||
Page toolbar can be set using the `Toolbar` property. |
|||
|
|||
**Example: Add a "New Item" toolbar item to the page toolbar.** |
|||
|
|||
Create a `PageToolbar` object and define toolbar items using the `AddButton` extension method. |
|||
|
|||
```csharp |
|||
public partial class Index |
|||
{ |
|||
protected PageToolbar Toolbar { get; } = new(); |
|||
|
|||
protected override void OnInitialized() |
|||
{ |
|||
Toolbar.AddButton("New Item", () => |
|||
{ |
|||
//Write your click action here |
|||
return Task.CompletedTask; |
|||
}, icon:IconName.Add); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Navigate back to the razor page and set the `Toolbar` parameter. |
|||
|
|||
```csharp |
|||
<PageHeader Toolbar="@Toolbar" /> |
|||
``` |
|||
|
|||
An example render result can be: |
|||
|
|||
 |
|||
|
|||
|
|||
@ -0,0 +1,126 @@ |
|||
# Page Toolbar Extensions for Blazor UI |
|||
|
|||
Page toolbar system allows you to add components to the toolbar of any page. The page toolbar is the area right to the header of a page. A button ("Import users from excel") was added to the user management page below: |
|||
|
|||
 |
|||
|
|||
You can add any type of view component item to the page toolbar or modify existing items. |
|||
|
|||
## How to Set Up |
|||
|
|||
In this example, we will add an "Import users from excel" button and execute a C# code for the user management page of the [Identity Module](../../Modules/Identity.md). |
|||
|
|||
### Create a C# File |
|||
|
|||
First, add a new C# file to your solution. We added inside the `/Pages/Identity/` folder of the `.Blazor` project: |
|||
|
|||
 |
|||
|
|||
We will use the [component override system](Customization-Overriding-Components.md) in the Blazor. After creating a class inherits from the `UserManagement` component, we will override the `SetToolbarItemsAsync` method and add the toolbar item programmatically. |
|||
|
|||
Here, the content of the overridden `SetToolbarItemsAsync` method. |
|||
|
|||
```csharp |
|||
protected override async ValueTask SetToolbarItemsAsync() |
|||
{ |
|||
await base.SetToolbarItemsAsync(); |
|||
Toolbar.AddButton("Import users from excel", () => |
|||
{ |
|||
//TODO: Write your custom code |
|||
return Task.CompletedTask; |
|||
}, "file-import", Blazorise.Color.Secondary); |
|||
} |
|||
``` |
|||
> In order to use the `AddButton` extension method, you need to add a using statement for the `Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars` namespace. |
|||
|
|||
Here, the entire content of the file. |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity.Blazor.Pages.Identity; |
|||
|
|||
namespace MyCompanyName.MyProjectName.Blazor.Pages.Identity |
|||
{ |
|||
[ExposeServices(typeof(UserManagement))] |
|||
[Dependency(ReplaceServices = true)] |
|||
public class CustomizedUserManagement : UserManagement |
|||
{ |
|||
protected override async ValueTask SetToolbarItemsAsync() |
|||
{ |
|||
await base.SetToolbarItemsAsync(); |
|||
Toolbar.AddButton("Import users from excel", () => |
|||
{ |
|||
//TODO: Write your custom code |
|||
return Task.CompletedTask; |
|||
}, "file-import", Blazorise.Color.Secondary); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
When you run the application, you will see the button added next to the current button list. There are some other parameters of the `AddButton` method (for example, use `Order` to set the order of the button component relative to the other components). |
|||
|
|||
## Advanced Use Cases |
|||
|
|||
While you typically want to add a button action to the page toolbar, it is possible to add any type of blazor component. |
|||
|
|||
### Add A Blazor Component to a Page Toolbar |
|||
|
|||
First, create a new blazor component in your project: |
|||
|
|||
 |
|||
|
|||
For this example, we've created a `MyToolbarComponent` component under the `/Pages/Identity/` folder. |
|||
|
|||
`MyToolbarComponent.razor` content: |
|||
|
|||
````csharp |
|||
<Button Color="Color.Dark">CLICK ME</Button> |
|||
```` |
|||
We will leave the `MyToolbarComponent.razor.cs` file empty. |
|||
|
|||
Then you can add the `MyToolbarComponent` to the user management page toolbar: |
|||
|
|||
````csharp |
|||
protected override async ValueTask SetToolbarItemsAsync() |
|||
{ |
|||
await base.SetToolbarItemsAsync(); |
|||
Toolbar.AddComponent<MyToolbarComponent>(); |
|||
} |
|||
```` |
|||
|
|||
* If your component accepts parameters, you can pass them as key/value pairs using the `arguments` parameter. |
|||
|
|||
#### Permissions |
|||
|
|||
If your button/component should be available based on a [permission/policy](../../Authorization.md), you can pass the permission/policy name as the `RequiredPolicyName` parameter to the `AddButton` and `AddComponent` methods. |
|||
|
|||
### Add a Page Toolbar Contributor |
|||
|
|||
If you perform advanced custom logic while adding an item to a page toolbar, you can create a class that implements the `IPageToolbarContributor` interface or inherits from the `PageToolbarContributor` class: |
|||
|
|||
````csharp |
|||
public class MyToolbarContributor : PageToolbarContributor |
|||
{ |
|||
public override Task ContributeAsync(PageToolbarContributionContext context) |
|||
{ |
|||
context.Items.Insert(0, new PageToolbarItem(typeof(MyToolbarComponent))); |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* You can use `context.ServiceProvider` to resolve dependencies if you need. |
|||
|
|||
Then add your class to the `Contributors` list: |
|||
|
|||
````csharp |
|||
protected override async ValueTask SetToolbarItemsAsync() |
|||
{ |
|||
await base.SetToolbarItemsAsync(); |
|||
Toolbar.Contributors.Add(new PageContributor()); |
|||
} |
|||
```` |
|||
|
|||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 984 B |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 60 KiB |
@ -0,0 +1,120 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc.ModelBinding; |
|||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters |
|||
{ |
|||
public class AbpRemoteStreamContentModelBinder : IModelBinder |
|||
{ |
|||
public async Task BindModelAsync(ModelBindingContext bindingContext) |
|||
{ |
|||
if (bindingContext == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(bindingContext)); |
|||
} |
|||
|
|||
var postedFiles = new List<IRemoteStreamContent>(); |
|||
|
|||
// If we're at the top level, then use the FieldName (parameter or property name).
|
|||
// This handles the fact that there will be nothing in the ValueProviders for this parameter
|
|||
// and so we'll do the right thing even though we 'fell-back' to the empty prefix.
|
|||
var modelName = bindingContext.IsTopLevelObject |
|||
? bindingContext.BinderModelName ?? bindingContext.FieldName |
|||
: bindingContext.ModelName; |
|||
|
|||
await GetFormFilesAsync(modelName, bindingContext, postedFiles); |
|||
|
|||
// If ParameterBinder incorrectly overrode ModelName, fall back to OriginalModelName prefix. Comparisons
|
|||
// are tedious because e.g. top-level parameter or property is named Blah and it contains a BlahBlah
|
|||
// property. OriginalModelName may be null in tests.
|
|||
if (postedFiles.Count == 0 && |
|||
bindingContext.OriginalModelName != null && |
|||
!string.Equals(modelName, bindingContext.OriginalModelName, StringComparison.Ordinal) && |
|||
!modelName.StartsWith(bindingContext.OriginalModelName + "[", StringComparison.Ordinal) && |
|||
!modelName.StartsWith(bindingContext.OriginalModelName + ".", StringComparison.Ordinal)) |
|||
{ |
|||
modelName = ModelNames.CreatePropertyModelName(bindingContext.OriginalModelName, modelName); |
|||
await GetFormFilesAsync(modelName, bindingContext, postedFiles); |
|||
} |
|||
|
|||
object value; |
|||
if (bindingContext.ModelType == typeof(IRemoteStreamContent) || bindingContext.ModelType == typeof(RemoteStreamContent)) |
|||
{ |
|||
if (postedFiles.Count == 0) |
|||
{ |
|||
// Silently fail if the named file does not exist in the request.
|
|||
return; |
|||
} |
|||
|
|||
value = postedFiles.First(); |
|||
} |
|||
else |
|||
{ |
|||
if (postedFiles.Count == 0 && !bindingContext.IsTopLevelObject) |
|||
{ |
|||
// Silently fail if no files match. Will bind to an empty collection (treat empty as a success
|
|||
// case and not reach here) if binding to a top-level object.
|
|||
return; |
|||
} |
|||
|
|||
// Perform any final type mangling needed.
|
|||
var modelType = bindingContext.ModelType; |
|||
if (modelType == typeof(IRemoteStreamContent[]) || modelType == typeof(RemoteStreamContent[])) |
|||
{ |
|||
value = postedFiles.ToArray(); |
|||
} |
|||
else |
|||
{ |
|||
value = postedFiles; |
|||
} |
|||
} |
|||
|
|||
// We need to add a ValidationState entry because the modelName might be non-standard. Otherwise
|
|||
// the entry we create in model state might not be marked as valid.
|
|||
bindingContext.ValidationState.Add(value, new ValidationStateEntry() |
|||
{ |
|||
Key = modelName, |
|||
}); |
|||
|
|||
bindingContext.ModelState.SetModelValue( |
|||
modelName, |
|||
rawValue: null, |
|||
attemptedValue: null); |
|||
|
|||
bindingContext.Result = ModelBindingResult.Success(value); |
|||
} |
|||
|
|||
private async Task GetFormFilesAsync( |
|||
string modelName, |
|||
ModelBindingContext bindingContext, |
|||
ICollection<IRemoteStreamContent> postedFiles) |
|||
{ |
|||
var request = bindingContext.HttpContext.Request; |
|||
if (request.HasFormContentType) |
|||
{ |
|||
var form = await request.ReadFormAsync(); |
|||
|
|||
foreach (var file in form.Files) |
|||
{ |
|||
// If there is an <input type="file" ... /> in the form and is left blank.
|
|||
if (file.Length == 0 && string.IsNullOrEmpty(file.FileName)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
postedFiles.Add(new RemoteStreamContent(file.OpenReadStream()) |
|||
{ |
|||
ContentType = file.ContentType |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Microsoft.AspNetCore.Mvc.ModelBinding; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters |
|||
{ |
|||
public class AbpRemoteStreamContentModelBinderProvider : IModelBinderProvider |
|||
{ |
|||
public IModelBinder GetBinder(ModelBinderProviderContext context) |
|||
{ |
|||
if (context == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
if (context.Metadata.ModelType == typeof(IRemoteStreamContent) || |
|||
context.Metadata.ModelType == typeof(RemoteStreamContent) || |
|||
typeof(IEnumerable<IRemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType) || |
|||
typeof(IEnumerable<RemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType)) |
|||
{ |
|||
return new AbpRemoteStreamContentModelBinder(); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc.Formatters; |
|||
using Microsoft.Net.Http.Headers; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters |
|||
{ |
|||
public class RemoteStreamContentInputFormatter : InputFormatter |
|||
{ |
|||
public RemoteStreamContentInputFormatter() |
|||
{ |
|||
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("*/*")); |
|||
} |
|||
|
|||
protected override bool CanReadType(Type type) |
|||
{ |
|||
return type == typeof(IRemoteStreamContent) || |
|||
type == typeof(RemoteStreamContent); |
|||
} |
|||
|
|||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) |
|||
{ |
|||
return InputFormatterResult.SuccessAsync(new RemoteStreamContent(context.HttpContext.Request.Body) |
|||
{ |
|||
ContentType = context.HttpContext.Request.ContentType |
|||
}); |
|||
} |
|||
} |
|||
} |
|||