@ -0,0 +1,87 @@ |
|||
# ABP.IO Platform 4.0 with .NET 5.0 in the 4th Year! |
|||
|
|||
Today, we are extremely happy to release ABP Framework 4.0 with **.NET 5.0 support**! |
|||
|
|||
## 4 Years of Work |
|||
|
|||
As a nice coincidence, today is the **4th year** since the first commit made in the [abp repository](https://github.com/abpframework/abp)! So, we can say "*Happy Birthday ABP Framework!*". |
|||
|
|||
 |
|||
|
|||
### Some Statistics |
|||
|
|||
ABP.IO Platform and the ABP Community is growing. Here, a summary of these 4 years. |
|||
|
|||
From GitHub, only from the main [abp repository](https://github.com/abpframework/abp); |
|||
|
|||
* **15,297 commits** done. |
|||
* **3,764 issues** are closed. |
|||
* **2,133 pull requests** are merged. |
|||
* **158 contributors**. |
|||
* **88 releases** published. |
|||
* **5.2K stars** on GitHub. |
|||
|
|||
From NuGet & NPM; |
|||
|
|||
* **220 NuGet** packages & **52 NPM** packages. |
|||
* **1,000,000 downloads** only for the core NuGet package. |
|||
|
|||
From Website; |
|||
|
|||
* **200,000 visitors**. |
|||
* **1,000,000+ sessions**. |
|||
|
|||
## What's New With 4.0? |
|||
|
|||
Since all the new features are already explained in details with the [4.0 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.0-RC-Has-Been-Released-based-on-.NET-5.0), I will not repeat all the details again. Please read [the RC post](https://blog.abp.io/abp/ABP.IO-Platform-v4.0-RC-Has-Been-Released-based-on-.NET-5.0) for **new feature and changes** you may need to do for your solution while upgrading to the version 4.0. |
|||
|
|||
Here, a brief list of major features and changes; |
|||
|
|||
* Migrated to **.NET 5.0**. |
|||
* Stable **Blazor** UI. |
|||
* Moved to **System.Text.Json**. |
|||
* Upgraded to **IdentityServer** version 4.0. |
|||
* **WPF** startup template. |
|||
|
|||
## Creating New Solutions |
|||
|
|||
You can create a new solution with the ABP Framework version 4.0 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 |
|||
|
|||
This is a **major version** and requires some **manual work**, especially related to **.NET 5.0** and **IdentityServer** 4.0 upgrades. |
|||
|
|||
* See the [MIGRATION GUIDE](https://docs.abp.io/en/abp/latest/Migration-Guides/Abp-4_0) that covers all the details about the upgrade progress. |
|||
|
|||
* You can also see the [upgrading document](https://docs.abp.io/en/abp/latest/Upgrading). |
|||
|
|||
## New Guides / Documents |
|||
|
|||
We are constantly improving the documentation. Our purpose is not only document the ABP Framework, but also write architectural and practical guides for developers. |
|||
|
|||
### Implementing Domain Driven Design |
|||
|
|||
[Implementing Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design-Implementation-Guide) is a practical guide for they want to implement the DDD principles in their solutions. While the implementation details rely on the ABP Framework infrastructure, core concepts, principles and patterns are applicable in any kind of solution, even if it is not a .NET solution. |
|||
|
|||
 |
|||
|
|||
### Testing |
|||
|
|||
The new [Testing document](https://docs.abp.io/en/abp/latest/Testing) discusses different kind of automated tests and explains how you can write tests for your ABP based solutions. |
|||
|
|||
### UI Documents |
|||
|
|||
We've created a lot of documents for the [MVC](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Overall), [Blazor](https://docs.abp.io/en/abp/latest/UI/Blazor/Overall) and the [Angular](https://docs.abp.io/en/abp/latest/UI/Angular/Quick-Start) UI. |
|||
|
|||
## About the Next Version |
|||
|
|||
The next versions 4.1 will mostly focus on; |
|||
|
|||
* Improving current features. |
|||
* Complete module features for the Blazor UI. |
|||
* Improve developer experience and productivity. |
|||
* More documentation and examples. |
|||
|
|||
Planned preview date for the version **4.1 is December 17, 2020**. See the [Road Map](https://docs.abp.io/en/abp/latest/Road-Map) document and [GitHub Milestones](https://github.com/abpframework/abp/milestones) to learn what's planned for the next versions. We are trying to be clear about the coming features and the next release dates. |
|||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 149 KiB |
@ -0,0 +1,936 @@ |
|||
# Creating an Event Organizer Application with the ABP Framework & Blazor UI. |
|||
|
|||
## Introduction |
|||
|
|||
In this article, we will create an example application that is a simple **meeting/event organizer**: People create events and other people registers to the event. |
|||
|
|||
The application has been developed with **Blazor** as the UI framework and **MongoDB** as the database provider. |
|||
|
|||
> This tutorial is based on my notes that I'd created to implement this application in a workshop. It shows the necessary steps to build the application rather than detailed explanations. |
|||
|
|||
### Source Code |
|||
|
|||
Source code of the completed application is [available on GitHub](https://github.com/abpframework/abp-samples/tree/master/EventOrganizer). |
|||
|
|||
### Screenshots |
|||
|
|||
Here, the pages of the final application. |
|||
|
|||
**Home Page - Event List** |
|||
|
|||
 |
|||
|
|||
**Creating a new Event** |
|||
|
|||
 |
|||
|
|||
**Event Detail Page** |
|||
|
|||
 |
|||
|
|||
## Requirements |
|||
|
|||
The following tools are needed to be able to run the solution. |
|||
|
|||
* .NET 5.0 SDK |
|||
* Visual Studio 2019 16.8.0+ or another compatible IDE |
|||
* MongoDB Server (with MongoDB Compass) |
|||
|
|||
## Development |
|||
|
|||
### Creating a new Application |
|||
|
|||
* Use the following ABP CLI command: |
|||
|
|||
````bash |
|||
abp new EventOrganizer -u blazor -d mongodb |
|||
```` |
|||
|
|||
### Open & Run the Application |
|||
|
|||
* Open the solution in Visual Studio (or your favorite IDE). |
|||
* Run the `EventOrganizer.DbMigrator` application to seed the initial data. |
|||
* Run the `EventOrganizer.HttpApi.Host` application that starts the server side. |
|||
* Run the `EventOrganizer.Blazor` application to start the UI. |
|||
|
|||
### Apply the Custom Styles |
|||
|
|||
* Add styles to `wwwroot/main.css`: |
|||
|
|||
````css |
|||
body.abp-application-layout { |
|||
background-color: #222 !important; |
|||
font-size: 18px; |
|||
} |
|||
nav#main-navbar.bg-dark { |
|||
background-color: #222 !important; |
|||
box-shadow: none !important; |
|||
} |
|||
.event-pic { |
|||
width: 100%; |
|||
border-radius: 12px; |
|||
box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); |
|||
margin-bottom: 10px; |
|||
} |
|||
.event-link:hover, .event-link:hover *{ |
|||
text-decoration: none; |
|||
} |
|||
.event-link:hover .event-pic { |
|||
box-shadow: 5px 5px 0px 0px #ffd800; |
|||
} |
|||
.event-form { |
|||
background-color: #333 !important; |
|||
box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); |
|||
border-radius: 12px; |
|||
} |
|||
.table { |
|||
background: #fff; |
|||
border-radius: 12px; |
|||
box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); |
|||
} |
|||
.table th{ |
|||
border: 0 !important; |
|||
} |
|||
.modal { |
|||
color: #333; |
|||
} |
|||
.page-item:first-child .page-link { |
|||
margin-left: 0; |
|||
border-top-left-radius: 12px; |
|||
border-bottom-left-radius: 12px; |
|||
} |
|||
.page-item:last-child .page-link { |
|||
border-top-right-radius: 12px; |
|||
border-bottom-right-radius: 12px; |
|||
} |
|||
.btn { |
|||
border-radius: 8px; |
|||
} |
|||
.att-list { |
|||
list-style: none; |
|||
padding: 0; |
|||
} |
|||
.att-list li { |
|||
padding: 4px 0 0 0; |
|||
} |
|||
```` |
|||
|
|||
* `wwwroot/index.html`: Remove `bg-light` class from the `body` tag and add `bg-dark text-light`. |
|||
|
|||
### Domain Layer |
|||
|
|||
* Add the following `Event` aggregate (with `EventAttendee`) to the solution: |
|||
|
|||
**Event** |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Domain.Entities.Auditing; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class Event : FullAuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Title { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
|
|||
public bool IsFree { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
|
|||
public ICollection<EventAttendee> Attendees { get; set; } |
|||
|
|||
public Event() |
|||
{ |
|||
Attendees = new List<EventAttendee>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
**EventAttendee** |
|||
|
|||
```csharp |
|||
using System; |
|||
using Volo.Abp.Auditing; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAttendee : IHasCreationTime |
|||
{ |
|||
public Guid UserId { get; set; } |
|||
|
|||
public DateTime CreationTime { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### MongoDB Mapping |
|||
|
|||
* Add the following property to the `EventOrganizerMongoDbContext`: |
|||
|
|||
````csharp |
|||
public IMongoCollection<Event> Events => Collection<Event>(); |
|||
```` |
|||
|
|||
### Clean Index.razor & Add the Header & "Create Event" button |
|||
|
|||
* Clean the `Index.razor` file. |
|||
* Replace the content with the following code: |
|||
|
|||
````html |
|||
@page "/" |
|||
@inherits EventOrganizerComponentBase |
|||
<Row Class="mb-4"> |
|||
<Column Class="text-left"> |
|||
<h1>Upcoming Events</h1> |
|||
</Column> |
|||
<Column Class="text-right"> |
|||
@if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
<a class="btn btn-primary" href="/create-event"> |
|||
<i class="fa fa-plus"></i> @L["CreateEvent"] |
|||
</a> |
|||
} |
|||
</Column> |
|||
</Row> |
|||
```` |
|||
|
|||
* Open `Localization/EventOrganizer/en.json` in the `EventOrganizer.Domain.Shared` project and add the following entry: |
|||
|
|||
````json |
|||
"CreateEvent": "Create a new event!" |
|||
```` |
|||
|
|||
The Result (run the `EventOrganizer.Blazor` application to see): |
|||
|
|||
 |
|||
|
|||
### Event Creation |
|||
|
|||
* Create the Initial `IEventAppService` with the `CreateAsync` method: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public interface IEventAppService : IApplicationService |
|||
{ |
|||
Task<Guid> CreateAsync(EventCreationDto input); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add `EventCreationDto` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventCreationDto |
|||
{ |
|||
[Required] |
|||
[StringLength(100)] |
|||
public string Title { get; set; } |
|||
|
|||
[Required] |
|||
[StringLength(2000)] |
|||
public string Description { get; set; } |
|||
|
|||
public bool IsFree { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Implement the `EventAppService`: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAppService : EventOrganizerAppService, IEventAppService |
|||
{ |
|||
private readonly IRepository<Event, Guid> _eventRepository; |
|||
|
|||
public EventAppService(IRepository<Event, Guid> eventRepository) |
|||
{ |
|||
_eventRepository = eventRepository; |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task<Guid> CreateAsync(EventCreationDto input) |
|||
{ |
|||
var eventEntity = ObjectMapper.Map<EventCreationDto, Event>(input); |
|||
await _eventRepository.InsertAsync(eventEntity); |
|||
return eventEntity.Id; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add AutoMapper mapping to the `EventOrganizerApplicationAutoMapperProfile` class: |
|||
|
|||
````csharp |
|||
using AutoMapper; |
|||
using EventOrganizer.Events; |
|||
|
|||
namespace EventOrganizer |
|||
{ |
|||
public class EventOrganizerApplicationAutoMapperProfile : Profile |
|||
{ |
|||
public EventOrganizerApplicationAutoMapperProfile() |
|||
{ |
|||
CreateMap<EventCreationDto, Event>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This will automatically create the HTTP (REST) API for the application service (run the `EventOrganizer.HttpApi.Host` application to see it on the Swagger UI): |
|||
|
|||
 |
|||
|
|||
* Create the `CreateEvent.razor` file: |
|||
|
|||
````csharp |
|||
@page "/create-event" |
|||
@inherits EventOrganizerComponentBase |
|||
<Heading Size="HeadingSize.Is3" Margin="Margin.Is5.FromTop.Is4.FromBottom" Class="text-center">Create Event</Heading> |
|||
<Row> |
|||
<Column ColumnSize="ColumnSize.Is6.Is3.WithOffset"> |
|||
<div class="p-lg-5 p-md-3 event-form"> |
|||
<EditForm Model="@Event" OnValidSubmit="Create"> |
|||
<Field> |
|||
<FieldLabel>@L["Title"]</FieldLabel> |
|||
<TextEdit @bind-Text="@Event.Title" /> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["Description"]</FieldLabel> |
|||
<MemoEdit @bind-Text="@Event.Description" /> |
|||
</Field> |
|||
<Field> |
|||
<Check TValue="bool" @bind-Checked="@Event.IsFree">@L["Free"]</Check> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["StartTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTime" @bind-Date="@Event.StartTime" /> |
|||
</Field> |
|||
<Button Type="@ButtonType.Submit" Block="true" Color="@Color.Primary" Size="Size.Large">@L["Save"]</Button> |
|||
</EditForm> |
|||
</div> |
|||
</Column> |
|||
</Row> |
|||
```` |
|||
|
|||
* Create a partial `CreateEvent` class in the same folder, with the `CreateEvent.razor.cs` as the file name: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using EventOrganizer.Events; |
|||
using Microsoft.AspNetCore.Components; |
|||
|
|||
namespace EventOrganizer.Blazor.Pages |
|||
{ |
|||
public partial class CreateEvent |
|||
{ |
|||
private EventCreationDto Event { get; set; } = new EventCreationDto(); |
|||
|
|||
private readonly IEventAppService _eventAppService; |
|||
private readonly NavigationManager _navigationManager; |
|||
|
|||
public CreateEvent( |
|||
IEventAppService eventAppService, |
|||
NavigationManager navigationManager) |
|||
{ |
|||
_eventAppService = eventAppService; |
|||
_navigationManager = navigationManager; |
|||
} |
|||
|
|||
private async Task Create() |
|||
{ |
|||
var eventId = await _eventAppService.CreateAsync(Event); |
|||
_navigationManager.NavigateTo("/events/" + eventId); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
The final UI is (run the `EventOrganizer.Blazor` application and click to the "Create Event" button): |
|||
|
|||
 |
|||
|
|||
### Upcoming Events (Home Page) |
|||
|
|||
* Open the `IEventAppService` and add a `GetUpcomingAsync` method to get the list of upcoming events: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public interface IEventAppService : IApplicationService |
|||
{ |
|||
Task<Guid> CreateAsync(EventCreationDto input); |
|||
|
|||
Task<List<EventDto>> GetUpcomingAsync(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add a `EventDto` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventDto : EntityDto<Guid> |
|||
{ |
|||
public string Title { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
|
|||
public bool IsFree { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
|
|||
public int AttendeesCount { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Implement the `GetUpcomingAsync` in the `EventAppService` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAppService : EventOrganizerAppService, IEventAppService |
|||
{ |
|||
private readonly IRepository<Event, Guid> _eventRepository; |
|||
|
|||
public EventAppService(IRepository<Event, Guid> eventRepository) |
|||
{ |
|||
_eventRepository = eventRepository; |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task<Guid> CreateAsync(EventCreationDto input) |
|||
{ |
|||
var eventEntity = ObjectMapper.Map<EventCreationDto, Event>(input); |
|||
await _eventRepository.InsertAsync(eventEntity); |
|||
return eventEntity.Id; |
|||
} |
|||
|
|||
public async Task<List<EventDto>> GetUpcomingAsync() |
|||
{ |
|||
var events = await AsyncExecuter.ToListAsync( |
|||
_eventRepository |
|||
.Where(x => x.StartTime > Clock.Now) |
|||
.OrderBy(x => x.StartTime) |
|||
); |
|||
|
|||
return ObjectMapper.Map<List<Event>, List<EventDto>>(events); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add the following line into the `EventOrganizerApplicationAutoMapperProfile` constructor: |
|||
|
|||
````csharp |
|||
CreateMap<Event, EventDto>(); |
|||
```` |
|||
|
|||
Run the `EventOrganizer.HttpApi.Host` application to see the new `upcoming` endpoint on the Swagger UI: |
|||
|
|||
 |
|||
|
|||
* Change the `Pages/Index.razor.cs` content in the `EventOrganizer.Blazor` project as shown below: |
|||
|
|||
```csharp |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using EventOrganizer.Events; |
|||
|
|||
namespace EventOrganizer.Blazor.Pages |
|||
{ |
|||
public partial class Index |
|||
{ |
|||
private List<EventDto> UpcomingEvents { get; set; } = new List<EventDto>(); |
|||
|
|||
private readonly IEventAppService _eventAppService; |
|||
|
|||
public Index(IEventAppService eventAppService) |
|||
{ |
|||
_eventAppService = eventAppService; |
|||
} |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
UpcomingEvents = await _eventAppService.GetUpcomingAsync(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* Change the `Pages/Index.razor` content in the `EventOrganizer.Blazor` project as shown below: |
|||
|
|||
````html |
|||
@page "/" |
|||
@inherits EventOrganizerComponentBase |
|||
<Row Class="mb-4"> |
|||
<Column Class="text-left"> |
|||
<h1>Upcoming Events</h1> |
|||
</Column> |
|||
<Column Class="text-right"> |
|||
@if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
<a class="btn btn-primary" href="/create-event"> |
|||
<i class="fa fa-plus"></i> @L["CreateEvent"] |
|||
</a> |
|||
} |
|||
</Column> |
|||
</Row> |
|||
<Row> |
|||
@foreach (var upcomingEvent in UpcomingEvents) |
|||
{ |
|||
<Column Class="col-12 col-lg-4 col-md-6"> |
|||
<a class="mb-5 position-relative d-block event-link" href="/events/@upcomingEvent.Id"> |
|||
<div class="position-absolute text-right w-100 px-3 py-2" style="left: 0; top: 2px;"> |
|||
@if (upcomingEvent.IsFree) |
|||
{ |
|||
<Badge Color="Color.Success" Class="mr-1">FREE</Badge> |
|||
} |
|||
<span class="badge badge-warning font-weight-normal"> |
|||
<i class="fas fa-user-friends"></i> |
|||
<span class="font-weight-bold">@upcomingEvent.AttendeesCount</span> |
|||
</span> |
|||
</div> |
|||
<img src="https://picsum.photos/seed/@upcomingEvent.Id/400/300" class="event-pic"/> |
|||
<div class="px-3 py-1"> |
|||
<small class="font-weight-bold text-warning my-2 d-block text-uppercase">@upcomingEvent.StartTime.ToLongDateString()</small> |
|||
<p class="h4 text-light d-block mb-2">@upcomingEvent.Title</p> |
|||
<p class="text-light" style="opacity: .65;">@upcomingEvent.Description.TruncateWithPostfix(150)</p> |
|||
</div> |
|||
</a> |
|||
</Column> |
|||
} |
|||
</Row> |
|||
```` |
|||
|
|||
The new home page is shown below: |
|||
|
|||
 |
|||
|
|||
### Event Detail Page |
|||
|
|||
* Add `GetAsync`, `RegisterAsync`, `UnregisterAsync` and `DeleteAsync` methods to the `IEventAppService`: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public interface IEventAppService : IApplicationService |
|||
{ |
|||
Task<Guid> CreateAsync(EventCreationDto input); |
|||
|
|||
Task<List<EventDto>> GetUpcomingAsync(); |
|||
|
|||
Task<EventDetailDto> GetAsync(Guid id); |
|||
|
|||
Task RegisterAsync(Guid id); |
|||
|
|||
Task UnregisterAsync(Guid id); |
|||
|
|||
Task DeleteAsync(Guid id); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add `EventDetailDto` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventDetailDto : CreationAuditedEntityDto<Guid> |
|||
{ |
|||
public string Title { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
|
|||
public bool IsFree { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
|
|||
public List<EventAttendeeDto> Attendees { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add `EventAttendeeDto` class: |
|||
|
|||
````csharp |
|||
using System; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAttendeeDto |
|||
{ |
|||
public Guid UserId { get; set; } |
|||
|
|||
public string UserName { get; set; } |
|||
|
|||
public DateTime CreationTime { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Implement the new methods in the `EventAppService`: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using EventOrganizer.Users; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
public class EventAppService : EventOrganizerAppService, IEventAppService |
|||
{ |
|||
private readonly IRepository<Event, Guid> _eventRepository; |
|||
private readonly IRepository<AppUser, Guid> _userRepository; |
|||
|
|||
public EventAppService(IRepository<Event, Guid> eventRepository, IRepository<AppUser, Guid> userRepository) |
|||
{ |
|||
_eventRepository = eventRepository; |
|||
_userRepository = userRepository; |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task<Guid> CreateAsync(EventCreationDto input) |
|||
{ |
|||
var eventEntity = ObjectMapper.Map<EventCreationDto, Event>(input); |
|||
await _eventRepository.InsertAsync(eventEntity); |
|||
return eventEntity.Id; |
|||
} |
|||
|
|||
public async Task<List<EventDto>> GetUpcomingAsync() |
|||
{ |
|||
var events = await AsyncExecuter.ToListAsync( |
|||
_eventRepository |
|||
.Where(x => x.StartTime > Clock.Now) |
|||
.OrderBy(x => x.StartTime) |
|||
); |
|||
|
|||
return ObjectMapper.Map<List<Event>, List<EventDto>>(events); |
|||
} |
|||
|
|||
public async Task<EventDetailDto> GetAsync(Guid id) |
|||
{ |
|||
var @event = await _eventRepository.GetAsync(id); |
|||
var attendeeIds = @event.Attendees.Select(a => a.UserId).ToList(); |
|||
var attendees = (await AsyncExecuter.ToListAsync(_userRepository.Where(u => attendeeIds.Contains(u.Id)))) |
|||
.ToDictionary(x => x.Id); |
|||
|
|||
var result = ObjectMapper.Map<Event, EventDetailDto>(@event); |
|||
|
|||
foreach (var attendeeDto in result.Attendees) |
|||
{ |
|||
attendeeDto.UserName = attendees[attendeeDto.UserId].UserName; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task RegisterAsync(Guid id) |
|||
{ |
|||
var @event = await _eventRepository.GetAsync(id); |
|||
if (@event.Attendees.Any(a => a.UserId == CurrentUser.Id)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
@event.Attendees.Add(new EventAttendee {UserId = CurrentUser.GetId(), CreationTime = Clock.Now}); |
|||
await _eventRepository.UpdateAsync(@event); |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task UnregisterAsync(Guid id) |
|||
{ |
|||
var @event = await _eventRepository.GetAsync(id); |
|||
var removedItems = @event.Attendees.RemoveAll(x => x.UserId == CurrentUser.Id); |
|||
if (removedItems.Any()) |
|||
{ |
|||
await _eventRepository.UpdateAsync(@event); |
|||
} |
|||
} |
|||
|
|||
[Authorize] |
|||
public async Task DeleteAsync(Guid id) |
|||
{ |
|||
var @event = await _eventRepository.GetAsync(id); |
|||
|
|||
if (CurrentUser.Id != @event.CreatorId) |
|||
{ |
|||
throw new UserFriendlyException("You don't have the necessary permission to delete this event!"); |
|||
} |
|||
|
|||
await _eventRepository.DeleteAsync(id); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Add the following mappings into the `EventOrganizerApplicationAutoMapperProfile`: |
|||
|
|||
````csharp |
|||
CreateMap<Event, EventDetailDto>(); |
|||
CreateMap<EventAttendee, EventAttendeeDto>(); |
|||
```` |
|||
|
|||
Run the `EventOrganizer.HttpApi.Host` application to see the complete Event HTTP API in the Swagger UI: |
|||
|
|||
 |
|||
|
|||
* Create `EventDetail.razor` component with the following content: |
|||
|
|||
````html |
|||
@page "/events/{id}" |
|||
@inherits EventOrganizerComponentBase |
|||
@if (Event != null) |
|||
{ |
|||
<Row Class="mb-4"> |
|||
<Column Class="text-left"> |
|||
<h1>@Event.Title</h1> |
|||
</Column> |
|||
<Column Class="text-right pt-2"> |
|||
<a href="/" Class="btn btn-dark"><i class="fa fa-arrow-left"></i> Back</a> |
|||
@if (CurrentUser.IsAuthenticated && CurrentUser.Id == Event.CreatorId) |
|||
{ |
|||
<Button Color="Color.Danger" Clicked="Delete" Class="ml-1">Delete</Button> |
|||
} |
|||
</Column> |
|||
</Row> |
|||
<Row> |
|||
<Column Class="col-12 col-md-8"> |
|||
<div class="position-relative"> |
|||
<div class="position-absolute text-right w-100 px-3 py-2" style="left: 0; top: 2px;"> |
|||
@if (Event.IsFree) |
|||
{ |
|||
<Badge Color="Color.Success" Class="mr-1">FREE</Badge> |
|||
} |
|||
<span class="badge badge-warning font-weight-normal"> |
|||
<i class="fas fa-user-friends"></i> |
|||
<span class="font-weight-bold">@Event.Attendees.Count</span> |
|||
</span> |
|||
</div> |
|||
<img src="https://picsum.photos/seed/@Event.Id/800/600" class="event-pic" /> |
|||
<small class="font-weight-bold text-warning my-2 d-block text-uppercase">Start time: @Event.StartTime.ToLongDateString()</small> |
|||
<p style="opacity: .65;">@Event.Description</p> |
|||
</div> |
|||
</Column> |
|||
<Column Class="col-12 col-md-4"> |
|||
<div class="p-4 event-form"> |
|||
@if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
<div> |
|||
@if (!IsRegistered) |
|||
{ |
|||
<Button Color="Color.Primary" Clicked="Register" Class="btn-block btn-lg">Register now!</Button> |
|||
} |
|||
else |
|||
{ |
|||
<p>You are registered in this event</p> |
|||
<Button Color="Color.Secondary" Clicked="UnRegister" Class="btn-block">Cancel registration!</Button> |
|||
} |
|||
</div> |
|||
} |
|||
else |
|||
{ |
|||
<a class="btn btn-primary" href="/authentication/login"> |
|||
<i class="fa fa-sign-in-alt"></i> Login to attend! |
|||
</a> |
|||
} |
|||
</div> |
|||
<div class="mt-4 event-form p-4"> |
|||
<span class="font-weight-bold"><i class="fas fa-user-friends"></i> Attendees <span class="float-right font-weight-normal" style="opacity:.65;">(@Event.Attendees.Count)</span></span> |
|||
<ul class="mt-1 mb-0 att-list"> |
|||
@foreach (var attendee in Event.Attendees) |
|||
{ |
|||
<li><i class="fa fa-check"></i> @attendee.UserName</li> |
|||
} |
|||
</ul> |
|||
</div> |
|||
</Column> |
|||
</Row> |
|||
} |
|||
```` |
|||
|
|||
* Create `EventDetail.razor.cs` file with the following content: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using EventOrganizer.Events; |
|||
using Microsoft.AspNetCore.Components; |
|||
|
|||
namespace EventOrganizer.Blazor.Pages |
|||
{ |
|||
public partial class EventDetail |
|||
{ |
|||
[Parameter] |
|||
public string Id { get; set; } |
|||
|
|||
private EventDetailDto Event { get; set; } |
|||
private bool IsRegistered { get; set; } |
|||
|
|||
private readonly IEventAppService _eventAppService; |
|||
private readonly NavigationManager _navigationManager; |
|||
|
|||
public EventDetail( |
|||
IEventAppService eventAppService, |
|||
NavigationManager navigationManager) |
|||
{ |
|||
_eventAppService = eventAppService; |
|||
_navigationManager = navigationManager; |
|||
} |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
await GetEventAsync(); |
|||
} |
|||
|
|||
private async Task GetEventAsync() |
|||
{ |
|||
Event = await _eventAppService.GetAsync(Guid.Parse(Id)); |
|||
if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
IsRegistered = Event.Attendees.Any(a => a.UserId == CurrentUser.Id); |
|||
} |
|||
} |
|||
|
|||
private async Task Register() |
|||
{ |
|||
await _eventAppService.RegisterAsync(Guid.Parse(Id)); |
|||
await GetEventAsync(); |
|||
} |
|||
|
|||
private async Task UnRegister() |
|||
{ |
|||
await _eventAppService.UnregisterAsync(Guid.Parse(Id)); |
|||
await GetEventAsync(); |
|||
} |
|||
|
|||
private async Task Delete() |
|||
{ |
|||
if (!await Message.Confirm("This event will be deleted: " + Event.Title)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await _eventAppService.DeleteAsync(Guid.Parse(Id)); |
|||
_navigationManager.NavigateTo("/"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
The resulting page is shown below: |
|||
|
|||
 |
|||
|
|||
### Integration Tests |
|||
|
|||
Create an `EventAppService_Tests` class in the `EventOrganizer.Application.Tests` project: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace EventOrganizer.Events |
|||
{ |
|||
[Collection(EventOrganizerTestConsts.CollectionDefinitionName)] |
|||
public class EventAppService_Tests : EventOrganizerApplicationTestBase |
|||
{ |
|||
private readonly IEventAppService _eventAppService; |
|||
|
|||
public EventAppService_Tests() |
|||
{ |
|||
_eventAppService = GetRequiredService<IEventAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Create_A_Valid_Event() |
|||
{ |
|||
// Create an event |
|||
|
|||
var eventId = await _eventAppService.CreateAsync( |
|||
new EventCreationDto |
|||
{ |
|||
Title = "My test event 1", |
|||
Description = "My test event description 1", |
|||
IsFree = true, |
|||
StartTime = DateTime.Now.AddDays(2) |
|||
} |
|||
); |
|||
|
|||
eventId.ShouldNotBe(Guid.Empty); |
|||
|
|||
// Get the event |
|||
|
|||
var @event = await _eventAppService.GetAsync(eventId); |
|||
@event.Title.ShouldBe("My test event 1"); |
|||
|
|||
// Get upcoming events |
|||
|
|||
var events = await _eventAppService.GetUpcomingAsync(); |
|||
events.ShouldContain(x => x.Title == "My test event 1"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Source Code |
|||
|
|||
Source code of the completed application is [available on GitHub](https://github.com/abpframework/abp-samples/tree/master/EventOrganizer). |
|||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 865 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,65 @@ |
|||
# Getting Started |
|||
|
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC", "Blazor", "NG"], |
|||
"DB": ["EF", "Mongo"], |
|||
"Tiered": ["Yes", "No"] |
|||
} |
|||
```` |
|||
|
|||
> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. |
|||
|
|||
## Create a New Project |
|||
|
|||
Use the `new` command of the ABP CLI to create a new project: |
|||
|
|||
````shell |
|||
abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes"}}{{if UI == "MVC"}} --tiered{{else}} --separate-identity-server{{end}}{{end}} |
|||
```` |
|||
|
|||
*You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.* |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. |
|||
|
|||
{{ else }} |
|||
|
|||
* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint on the server. |
|||
|
|||
{{ end }} |
|||
|
|||
{{ end }} |
|||
|
|||
> [ABP CLI document](./CLI.md) covers all of the available commands and options. |
|||
|
|||
> Alternatively, you can **create and download** projects from [ABP Framework website](https://abp.io/get-started) by easily selecting the all the options from the page. |
|||
|
|||
### The Solution Structure |
|||
|
|||
The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. |
|||
|
|||
{{ if DB == "Mongo" }} |
|||
|
|||
#### MongoDB Transactions |
|||
|
|||
The [startup template](Startup-templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable the it in the *YourProjectMongoDbModule* class's `ConfigureServices` method: |
|||
|
|||
```csharp |
|||
Configure<AbpUnitOfWorkDefaultOptions>(options => |
|||
{ |
|||
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; |
|||
}); |
|||
``` |
|||
|
|||
> Or you can delete that code since `Auto` is already the default behavior. |
|||
|
|||
{{ end }} |
|||
|
|||
## Next Step |
|||
|
|||
* [Running the solution](Getting-Started-Running-Solution.md) |
|||
@ -0,0 +1,217 @@ |
|||
# Getting Started |
|||
|
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC", "Blazor", "NG"], |
|||
"DB": ["EF", "Mongo"], |
|||
"Tiered": ["Yes", "No"] |
|||
} |
|||
```` |
|||
|
|||
> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. |
|||
|
|||
## Create the Database |
|||
|
|||
### Connection String |
|||
|
|||
Check the **connection string** in the `appsettings.json` file under the {{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}{{if UI=="MVC"}}`.Web` project{{else}}`.HttpApi.Host` project{{end}}{{end}} |
|||
|
|||
{{ if DB == "EF" }} |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
|||
} |
|||
```` |
|||
|
|||
The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). |
|||
|
|||
### Apply the Migrations |
|||
|
|||
The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). So, you need to apply migrations to create the database. There are two ways of applying the database migrations. |
|||
|
|||
#### Apply Migrations Using the DbMigrator |
|||
|
|||
The solution comes with a `.DbMigrator` console application which applies migrations and also **seeds the initial data**. It is useful on **development** as well as on **production** environment. |
|||
|
|||
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
|||
|
|||
Right click to the `.DbMigrator` project and select **Set as StartUp Project** |
|||
|
|||
 |
|||
|
|||
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
|||
|
|||
 |
|||
|
|||
> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. |
|||
|
|||
#### Using EF Core Update-Database Command |
|||
|
|||
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
Right click to the {{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}} project and select **Set as StartUp project**: |
|||
|
|||
{{ else if UI != "MVC" }} |
|||
|
|||
Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**: |
|||
|
|||
{{ end }} |
|||
|
|||
 |
|||
|
|||
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: |
|||
|
|||
 |
|||
|
|||
This will create a new database based on the configured connection string. |
|||
|
|||
> **Using the `.DbMigrator` tool is the suggested way**, because it also seeds the initial data to be able to properly run the web application. |
|||
> |
|||
> If you just use the `Update-Database` command, you will have an empty database, so you can not login to the application since there is no initial admin user in the database. You can use the `Update-Database` command in development time when you don't need to seed the database. However, using the `.DbMigrator` application is easier and you can always use it to migrate the schema and seed the database. |
|||
|
|||
{{ else if DB == "Mongo" }} |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "mongodb://localhost:27017/BookStore" |
|||
} |
|||
```` |
|||
|
|||
The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server. |
|||
|
|||
### Seed Initial Data |
|||
|
|||
The solution comes with a `.DbMigrator` console application which **seeds the initial data**. It is useful on **development** as well as on **production** environment. |
|||
|
|||
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
|||
|
|||
Right click to the `.DbMigrator` project and select **Set as StartUp Project** |
|||
|
|||
 |
|||
|
|||
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
|||
|
|||
 |
|||
|
|||
> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. |
|||
|
|||
{{ end }} |
|||
|
|||
## Run the Application |
|||
|
|||
{{ if UI == "MVC" }} |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
> Tiered solutions use **Redis** as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. |
|||
|
|||
1. Ensure that the `.IdentityServer` project is the startup project. Run this application that will open a **login** page in your browser. |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
You can login, but you cannot enter to the main application here. This is **just the authentication server**. |
|||
|
|||
2. Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. |
|||
|
|||
 |
|||
|
|||
This is the HTTP API that is used by the web application. |
|||
|
|||
3. Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser |
|||
|
|||
 |
|||
|
|||
Click to the **login** button which will redirect you to the *authentication server* to login to the application: |
|||
|
|||
 |
|||
|
|||
{{ else # Tiered != "Yes" }} |
|||
|
|||
Ensure that the `.Web` project is the startup project. Run the application which will open the **login** page in your browser: |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
 |
|||
|
|||
{{ end # Tiered }} |
|||
|
|||
{{ else # UI != "MVC" }} |
|||
|
|||
### Running the HTTP API Host (Server Side) |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
> Tiered solutions use Redis as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. |
|||
|
|||
Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
You can login, but you cannot enter to the main application here. This is just the authentication server. |
|||
|
|||
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: |
|||
|
|||
{{ else # Tiered == "No" }} |
|||
|
|||
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
{{ end # Tiered }} |
|||
|
|||
 |
|||
|
|||
You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. |
|||
|
|||
{{ end # UI }} |
|||
|
|||
{{ if UI == "Blazor" }} |
|||
|
|||
### Running the Blazor Application (Client Side) |
|||
|
|||
Ensure that the `.Blazor` project is the startup project and run the application. |
|||
|
|||
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
|||
|
|||
Once the application starts, click to the **Login** link on to header, which redirects you to the authentication server to enter a username and password: |
|||
|
|||
 |
|||
|
|||
{{ else if UI == "NG" }} |
|||
|
|||
### Running the Angular Application (Client Side) |
|||
|
|||
Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work) |
|||
|
|||
```bash |
|||
yarn |
|||
``` |
|||
|
|||
Once all node modules are loaded, execute `yarn start` (or `npm start`) command: |
|||
|
|||
```bash |
|||
yarn start |
|||
``` |
|||
|
|||
It may take a longer time for the first build. Once it finishes, it opens the Angular UI in your default browser with the [localhost:4200](http://localhost:4200/) address. |
|||
|
|||
 |
|||
|
|||
{{ end }} |
|||
|
|||
Enter **admin** as the username and **1q2w3E*** as the password to login to the application. The application is up and running. You can start developing your application based on this startup template. |
|||
|
|||
## Mobile Development |
|||
|
|||
If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends. |
|||
|
|||
See the [Getting Started with the React Native](Getting-Started-React-Native.md) document to learn how to configure and run the React Native application. |
|||
|
|||
## See Also |
|||
|
|||
* [Web Application Development Tutorial](Tutorials/Part-1.md) |
|||
* [Application Startup Template](Startup-Templates/Application.md) |
|||
@ -0,0 +1,56 @@ |
|||
# Getting Started |
|||
|
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC", "Blazor", "NG"], |
|||
"DB": ["EF", "Mongo"], |
|||
"Tiered": ["Yes", "No"] |
|||
} |
|||
```` |
|||
|
|||
> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. |
|||
|
|||
## Setup Your Development Environment |
|||
|
|||
First things first! Let's setup your development environment before creating the project. |
|||
|
|||
### Pre-Requirements |
|||
|
|||
The following tools should be installed on your development machine: |
|||
|
|||
* [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) (v16.8+) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). <sup id="a-editor">[1](#f-editor)</sup> |
|||
* [.NET Core 5.0+](https://www.microsoft.com/net/download/dotnet-core/) |
|||
{{ if UI != "Blazor" }} |
|||
* [Node v12 or v14](https://nodejs.org/) |
|||
* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) <sup id="a-yarn">[2](#f-yarn)</sup> or npm v6+ (already installed with Node) |
|||
{{ end }} |
|||
{{ if Tiered == "Yes" }} |
|||
* [Redis](https://redis.io/) (the startup solution uses the Redis as the [distributed cache](Caching.md)). |
|||
{{ end }} |
|||
|
|||
<sup id="f-editor"><b>1</b></sup> _You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core._ <sup>[↩](#a-editor)</sup> |
|||
|
|||
{{ if UI != "Blazor" }} |
|||
|
|||
<sup id="f-yarn"><b>2</b></sup> _Yarn v2 works differently and is not supported._ <sup>[↩](#a-yarn)</sup> |
|||
|
|||
{{ end }} |
|||
|
|||
### Install the ABP CLI |
|||
|
|||
[ABP CLI](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based solutions. First, you need to install the ABP CLI using the following command: |
|||
|
|||
````shell |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
If you've already installed, you can update it using the following command: |
|||
|
|||
````shell |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
## Next Step |
|||
|
|||
* [Creating a new solution](Getting-Started-Create-Solution.md) |
|||
@ -0,0 +1,5 @@ |
|||
# ABP Framework Migration Guides |
|||
|
|||
* [3.3.x to 4.0 Migration Guide](Abp-4_0.md) |
|||
* [2.9.x to 3.0 Migration Guide](../UI/Angular/Migration-Guide-v3.md) |
|||
|
|||
@ -1,3 +1,393 @@ |
|||
# Module Entity Extensions |
|||
|
|||
See https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions (it will be moved here soon). |
|||
## Introduction |
|||
|
|||
Module entity extension system is a **high level** extension system that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point. |
|||
|
|||
> The module must be developed the *Module Entity Extensions* system in mind. All the **official modules** supports this system wherever possible. |
|||
|
|||
## Quick Example |
|||
|
|||
Open the *YourProjectNameModuleExtensionConfigurator* class inside the `Domain.Shared` project of your solution and change the `ConfigureExtraProperties`method as shown below to add a `SocialSecurityNumber` property to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). |
|||
|
|||
````csharp |
|||
public static void ConfigureExtraProperties() |
|||
{ |
|||
OneTimeRunner.Run(() => |
|||
{ |
|||
ObjectExtensionManager.Instance.Modules() |
|||
.ConfigureIdentity(identity => |
|||
{ |
|||
identity.ConfigureUser(user => |
|||
{ |
|||
user.AddOrUpdateProperty<string>( //property type: string |
|||
"SocialSecurityNumber", //property name |
|||
property => |
|||
{ |
|||
//validation rules |
|||
property.Attributes.Add(new RequiredAttribute()); |
|||
property.Attributes.Add( |
|||
new StringLengthAttribute(64) { |
|||
MinimumLength = 4 |
|||
} |
|||
); |
|||
|
|||
//...other configurations for this property |
|||
} |
|||
); |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
>This method is called inside the `YourProjectNameDomainSharedModule` at the beginning of the application. `OneTimeRunner` is a utility class that guarantees to execute this code only one time per application, since multiple calls are unnecessary. |
|||
|
|||
* `ObjectExtensionManager.Instance.Modules()` is the starting point to configure a module. `ConfigureIdentity(...)` method is used to configure the entities of the Identity Module. |
|||
* `identity.ConfigureUser(...)` is used to configure the user entity of the identity module. Not all entities are designed to be extensible (since it is not needed). Use the intellisense to discover the extensible modules and entities. |
|||
* `user.AddOrUpdateProperty<string>(...)` is used to add a new property to the user entity with the `string` type (`AddOrUpdateProperty` method can be called multiple times for the same property of the same entity. Each call can configure the options of the same property, but only one property is added to the entity with the same property name). You can call this method with different property names to add more properties. |
|||
* `SocialSecurityNumber` is the name of the new property. |
|||
* `AddOrUpdateProperty` gets a second argument (the `property =>` lambda expression) to configure additional options for the new property. |
|||
* We can add data annotation attributes like shown here, just like adding a data annotation attribute to a class property. |
|||
|
|||
#### Create & Update Forms |
|||
|
|||
Once you define a property, it appears in the create and update forms of the related entity: |
|||
|
|||
 |
|||
|
|||
`SocialSecurityNumber` field comes into the form. Next sections will explain the localization and the validation for this new property. |
|||
|
|||
### Data Table |
|||
|
|||
New properties also appear in the data table of the related page: |
|||
|
|||
 |
|||
|
|||
`SocialSecurityNumber` column comes into the table. Next sections will explain the option to hide this column from the data table. |
|||
|
|||
## Property Options |
|||
|
|||
There are some options that you can configure while defining a new property. |
|||
|
|||
### Display Name |
|||
|
|||
You probably want to set a different (human readable) display name for the property that is shown on the user interface. |
|||
|
|||
#### Don't Want to Localize? |
|||
|
|||
If your application is not localized, you can directly set the `DisplayName` for the property to a `FixedLocalizableString` object. Example: |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.DisplayName = new FixedLocalizableString("Social security no"); |
|||
} |
|||
```` |
|||
|
|||
#### Localizing the Display Name |
|||
|
|||
If you want to localize the display name, you have two options. |
|||
|
|||
##### Localize by Convention |
|||
|
|||
Instead of setting the `property.DisplayName`, you can directly open your localization file (like `en.json`) and add the following entry to the `texts` section: |
|||
|
|||
````json |
|||
"SocialSecurityNumber": "Social security no" |
|||
```` |
|||
|
|||
Define the same `SocialSecurityNumber` key (the property name you've defined before) in your localization file for each language you support. That's all! |
|||
|
|||
In some cases, the localization key may conflict with other keys in your localization files. In such cases, you can use the `DisplayName:` prefix for display names in the localization file (`DisplayName:SocialSecurityNumber` as the localization key for this example). Extension system looks for prefixed version first, then fallbacks to the non prefixed name (it then fallbacks to the property name if you haven't localized it). |
|||
|
|||
> This approach is recommended since it is simple and suitable for most scenarios. |
|||
|
|||
##### Localize using the `DisplayName` Property |
|||
|
|||
If you want to specify the localization key or the localization resource, you can still set the `DisplayName` option: |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.DisplayName = |
|||
LocalizableString.Create<MyProjectNameResource>( |
|||
"UserSocialSecurityNumberDisplayName" |
|||
); |
|||
} |
|||
```` |
|||
|
|||
* `MyProjectNameResource` is the localization resource and `UserSocialSecurityNumberDisplayName` is the localization key in the localization resource. |
|||
|
|||
> See [the localization document](Localization.md) if you want to learn more about the localization system. |
|||
|
|||
#### Default Value |
|||
|
|||
A default value is automatically set for the new property, which is the natural default value for the property type, like `null` for `string`, `false` for `bool` or `0` for `int`. |
|||
|
|||
There are two ways to override the default value: |
|||
|
|||
##### DefaultValue Option |
|||
|
|||
`DefaultValue` option can be set to any value: |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.DefaultValue = 42; |
|||
} |
|||
```` |
|||
|
|||
##### DefaultValueFactory Options |
|||
|
|||
`DefaultValueFactory` can be set to a function that returns the default value: |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.DefaultValueFactory = () => DateTime.Now; |
|||
} |
|||
```` |
|||
|
|||
`options.DefaultValueFactory` has a higher priority than the `options.DefaultValue` . |
|||
|
|||
> Tip: Use `DefaultValueFactory` option only if the default value may change over the time (like `DateTime.Now` in this example). If it is a constant value, then use the `DefaultValue` option. |
|||
|
|||
### Validation |
|||
|
|||
Entity extension system allows you to define validation for extension properties in a few ways. |
|||
|
|||
#### Data Annotation Attributes |
|||
|
|||
`Attributes` is a list of attributes associated to this property. The example code below adds two [data annotation validation attributes](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) to the property: |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.Attributes.Add(new RequiredAttribute()); |
|||
property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4}); |
|||
} |
|||
```` |
|||
|
|||
When you run the application, you see that the validation works out of the box: |
|||
|
|||
 |
|||
|
|||
Since we've added the `RequiredAttribute`, it doesn't allow to left it blank. The validation system works; |
|||
|
|||
* On the user interface (with automatic localization). |
|||
* On the HTTP API. Even if you directly perform an HTTP request, you get validation errors with a proper HTTP status code. |
|||
* On the `SetProperty(...)` method on the entity (see [the document](Entities.md) if you wonder what is the `SetProperty()` method). |
|||
|
|||
So, it automatically makes a full stack validation. |
|||
|
|||
> See the [ASP.NET Core MVC Validation document](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) to learn more about the attribute based validation. |
|||
|
|||
##### Default Validation Attributes |
|||
|
|||
There are some attributes **automatically added** when you create certain type of properties; |
|||
|
|||
* `RequiredAttribute` is added for **non nullable** primitive property types (e.g. `int`, `bool`, `DateTime`...) and `enum` types. If you want to allow nulls, make the property nullable (e.g. `int?`). |
|||
* `EnumDataTypeAttribute` is added for **enum types**, to prevent to set invalid enum values. |
|||
|
|||
Use `property.Attributes.Clear();` if you don't want these attributes. |
|||
|
|||
#### Validation Actions |
|||
|
|||
Validation actions allows you to execute a custom code to perform the validation. The example below checks if the `SocialSecurityNumber` starts with `B` and adds a validation error if so: |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.Attributes.Add(new RequiredAttribute()); |
|||
property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4}); |
|||
|
|||
property.Validators.Add(context => |
|||
{ |
|||
if (((string) context.Value).StartsWith("B")) |
|||
{ |
|||
context.ValidationErrors.Add( |
|||
new ValidationResult( |
|||
"Social security number can not start with the letter 'B', sorry!", |
|||
new[] {"extraProperties.SocialSecurityNumber"} |
|||
) |
|||
); |
|||
} |
|||
}); |
|||
|
|||
} |
|||
```` |
|||
|
|||
Using a `RegularExpressionAttribute` might be better in this case, but this is just an example. Anyway, if you enter a value starts with the letter `B` you get the following error **while saving the form**: |
|||
|
|||
 |
|||
|
|||
##### The Context Object |
|||
|
|||
The `context` object has useful properties that can be used in your custom validation action. For example, you can use the `context.ServiceProvider` to resolve services from the [dependency injection system](Dependency-Injection.md). The example below gets the localizer and adds a localized error message: |
|||
|
|||
````csharp |
|||
if (((string) context.Value).StartsWith("B")) |
|||
{ |
|||
var localizer = context.ServiceProvider |
|||
.GetRequiredService<IStringLocalizer<MyProjectNameResource>>(); |
|||
|
|||
context.ValidationErrors.Add( |
|||
new ValidationResult( |
|||
localizer["SocialSecurityNumberCanNotStartWithB"], |
|||
new[] {"extraProperties.SocialSecurityNumber"} |
|||
) |
|||
); |
|||
} |
|||
```` |
|||
|
|||
>`context.ServiceProvider` is nullable! It can be `null` only if you use the `SetProperty(...)` method on the object. Because DI system is not available on this time. While this is a rare case, you should perform a fallback logic when `context.ServiceProvider` is `null`. For this example, you would add a non-localized error message. This is not a problem since setting an invalid value to a property generally is a programmer mistake and you mostly don't need to localization in this case. In any way, you would not be able to use localization even in a regular property setter. But, if you are serious about localization, you can throw a business exception (see the [exception handling document](https://docs.abp.io/en/abp/latest/Exception-Handling) to learn how to localize a business exception). |
|||
|
|||
### UI Visibility |
|||
|
|||
When you define a property, it appears on the data table, create and edit forms on the related UI page. However, you can control each one individually. Example: |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.UI.OnTable.IsVisible = false; |
|||
//...other configurations |
|||
} |
|||
```` |
|||
|
|||
Use `property.UI.OnCreateForm` and `property.UI.OnEditForm` to control forms too. If a property is required, but not added to the create form, you definitely get a validation exception, so use this option carefully. But a required property may not be in the edit form if that's your requirement. |
|||
|
|||
### HTTP API Availability |
|||
|
|||
Even if you disable a property on UI, it can be still available through the HTTP API. By default, a property is available on all APIs. |
|||
|
|||
Use the `property.Api` options to make a property unavailable in some API endpoints. |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.Api.OnUpdate.IsAvailable = false; |
|||
} |
|||
```` |
|||
|
|||
In this example, Update HTTP API will not allow to set a new value to this property. In this case, you also want to disable this property on the edit form: |
|||
|
|||
````csharp |
|||
property => |
|||
{ |
|||
property.Api.OnUpdate.IsAvailable = false; |
|||
property.UI.OnEditForm.IsVisible = false; |
|||
} |
|||
```` |
|||
|
|||
In addition to the `property.Api.OnUpdate`, you can set `property.Api.OnCreate` and `property.Api.OnGet` for a fine control the API endpoint. |
|||
|
|||
## Special Types |
|||
|
|||
### Enum |
|||
|
|||
Module extension system naturally supports the `enum` types. |
|||
|
|||
An example enum type: |
|||
|
|||
````csharp |
|||
public enum UserType |
|||
{ |
|||
Regular, |
|||
Moderator, |
|||
SuperUser |
|||
} |
|||
```` |
|||
|
|||
You can add enum properties just like others: |
|||
|
|||
````csharp |
|||
user.AddOrUpdateProperty<UserType>("Type"); |
|||
```` |
|||
|
|||
An enum properties is shown as combobox (select) in the create/edit forms: |
|||
|
|||
 |
|||
|
|||
#### Localization |
|||
|
|||
Enum member name is shown on the table and forms by default. If you want to localize it, just create a new entry on your [localization](https://docs.abp.io/en/abp/latest/Localization) file: |
|||
|
|||
````json |
|||
"UserType.SuperUser": "Super user" |
|||
```` |
|||
|
|||
One of the following names can be used as the localization key: |
|||
|
|||
* `Enum:UserType.SuperUser` |
|||
* `UserType.SuperUser` |
|||
* `SuperUser` |
|||
|
|||
Localization system searches for the key with the given order. Localized text are used on the table and the create/edit forms. |
|||
|
|||
## Database Mapping |
|||
|
|||
For relational databases, all extension property values are stored in a single field in the table: |
|||
|
|||
 |
|||
|
|||
`ExtraProperties` field stores the properties as a JSON object. While that's fine for some scenarios, you may want to create a dedicated field for your new property. Fortunately, it is very easy to configure. |
|||
|
|||
If you are using the Entity Framework Core database provider, you can configure the database mapping as shown below: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.MapEfCoreProperty<IdentityUser, string>( |
|||
"SocialSecurityNumber", |
|||
(entityBuilder, propertyBuilder) => |
|||
{ |
|||
propertyBuilder.HasMaxLength(64); |
|||
} |
|||
); |
|||
```` |
|||
|
|||
Write this inside the `YourProjectNameEfCoreEntityExtensionMappings` class in your `.EntityFrameworkCore` project. Then you need to use the standard `Add-Migration` and `Update-Database` commands to create a new database migration and apply the change to your database. |
|||
|
|||
Add-Migration create a new migration as shown below: |
|||
|
|||
````csharp |
|||
public partial class Added_SocialSecurityNumber_To_IdentityUser : Migration |
|||
{ |
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AddColumn<string>( |
|||
name: "SocialSecurityNumber", |
|||
table: "AbpUsers", |
|||
maxLength: 128, |
|||
nullable: true); |
|||
} |
|||
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropColumn( |
|||
name: "SocialSecurityNumber", |
|||
table: "AbpUsers"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Once you update your database, you will see that the `AbpUsers` table has the new property as a standard table field: |
|||
|
|||
 |
|||
|
|||
> If you first created a property without a database table field, then you later needed to move this property to a database table field, it is suggested to execute an SQL command in your migration to copy the old values to the new field. |
|||
> |
|||
> However, if you don't make it, the ABP Framework seamlessly manages it. It uses the new database field, but fallbacks to the `ExtraProperties` field if it is null. When you save the entity, it moves the value to the new field. |
|||
|
|||
See the [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) document for more. |
|||
|
|||
## More |
|||
|
|||
See the [Customizing the Modules](Customizing-Application-Modules-Guide.md) guide for an overall index for all the extensibility options. |
|||
|
|||
Here, a few things you can do: |
|||
|
|||
* You can create a second entity that maps to the same database table with the extra property as a standard class property (if you've defined the EF Core mapping). For the example above, you can add a `public string SocialSecurityNumber {get; set;}` property to the `AppUser` entity in your application, since the `AppUser` entity is mapped to the same `AbpUser` table. Do this only if you need it, since it brings more complexity to your application. |
|||
* You can override a domain or application service to perform custom logics with your new property. |
|||
* You can low level control how to add/render a field in the data table on the UI. |
|||
|
|||
|
|||
@ -0,0 +1,3 @@ |
|||
# Swagger UI Integration |
|||
|
|||
TODO |
|||
@ -0,0 +1,161 @@ |
|||
# Data Table Column Extensions for ASP.NET Core UI |
|||
|
|||
## Introduction |
|||
|
|||
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 "Social security no" 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 JavaScript File |
|||
|
|||
First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: |
|||
|
|||
 |
|||
|
|||
Here, the content of this JavaScript file: |
|||
|
|||
```js |
|||
abp.ui.extensions.tableColumns |
|||
.get('identity.user') |
|||
.addContributor(function (columnList) { |
|||
columnList.addTail({ //add as the last column |
|||
title: 'Social security no', |
|||
data: 'extraProperties.SocialSecurityNumber', |
|||
orderable: false, |
|||
render: function (data, type, row) { |
|||
if (row.extraProperties.SocialSecurityNumber) { |
|||
return '<strong>' + |
|||
row.extraProperties.SocialSecurityNumber + |
|||
'<strong>'; |
|||
} else { |
|||
return '<i class="text-muted">undefined</i>'; |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
This example defines a custom `render` function to return a custom HTML to render in the column. |
|||
|
|||
### Add the File to the User Management Page |
|||
|
|||
Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification). |
|||
|
|||
Write the following code inside the `ConfigureServices` of your module class: |
|||
|
|||
```csharp |
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options.ScriptBundles.Configure( |
|||
typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, |
|||
bundleConfiguration => |
|||
{ |
|||
bundleConfiguration.AddFiles( |
|||
"/Pages/Identity/Users/my-user-extensions.js" |
|||
); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. |
|||
|
|||
### Rendering the Column |
|||
|
|||
This example assumes that you've defined a `SocialSecurityNumber` extra property using the [module entity extension](../../Module-Entity-Extensions.md) system. However; |
|||
|
|||
* You can add a new column that is related to an existing property of the user (that was not added to the table by default). Example: |
|||
|
|||
````js |
|||
abp.ui.extensions.tableColumns |
|||
.get('identity.user') |
|||
.addContributor(function (columnList) { |
|||
columnList.addTail({ |
|||
title: 'Phone confirmed?', |
|||
data: 'phoneNumberConfirmed', |
|||
render: function (data, type, row) { |
|||
if (row.phoneNumberConfirmed) { |
|||
return '<strong style="color: green">YES<strong>'; |
|||
} else { |
|||
return '<i class="text-muted">NO</i>'; |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
* You can add a new custom column that is not related to any entity property, but a completely custom information. Example: |
|||
|
|||
````js |
|||
abp.ui.extensions.tableColumns |
|||
.get('identity.user') |
|||
.addContributor(function (columnList) { |
|||
columnList.addTail({ |
|||
title: 'Custom column', |
|||
data: {}, |
|||
orderable: false, |
|||
render: function (data) { |
|||
if (data.phoneNumber) { |
|||
return "call: " + data.phoneNumber; |
|||
} else { |
|||
return ''; |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
## API |
|||
|
|||
This section explains details of the `abp.ui.extensions.tableColumns` JavaScript API. |
|||
|
|||
### abp.ui.extensions.tableColumns.get(entityName) |
|||
|
|||
This method is used to access the table columns for an entity of a specific module. It takes one parameter: |
|||
|
|||
* **entityName**: The name of the entity defined by the related module. |
|||
|
|||
### abp.ui.extensions.tableColumns.get(entityName).columns |
|||
|
|||
The `columns` property is used to retrieve a [doubly linked list](../Common/Utils/Linked-List.md) of previously defined columns for a table. All contributors are executed in order to prepare the final column list. This is normally called by the modules to show the columns in the table. However, you can use it if you are building your own extensible UIs. |
|||
|
|||
### abp.ui.extensions.tableColumns.get(entityName).addContributor(contributeCallback [, order]) |
|||
|
|||
The `addContributor` method covers all scenarios, e.g. you want to add your column in a different position in the list, change or remove an existing column. `addContributor` has the following parameters: |
|||
|
|||
* **contributeCallback**: A callback function that is called whenever the column list should be created. You can freely modify the column list inside this callback method. |
|||
* **order** (optional): The order of the callback in the callback list. Your callback is added to the end of the list (so, you have opportunity to modify columns added by the previous contributors). You can set it `0` to add your contributor as the first item. |
|||
|
|||
#### Example |
|||
|
|||
```js |
|||
var myColumnDefinition = { |
|||
title: 'Custom column', |
|||
data: {}, |
|||
orderable: false, |
|||
render: function(data) { |
|||
if (data.phoneNumber) { |
|||
return "call: " + data.phoneNumber; |
|||
} else { |
|||
return ''; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
abp.ui.extensions.tableColumns |
|||
.get('identity.user') |
|||
.addContributor(function (columnList) { |
|||
// Remove an item from actionList |
|||
columnList.dropHead(); |
|||
|
|||
// Add a new item to the actionList |
|||
columnList.addHead(myColumnDefinition); |
|||
}); |
|||
``` |
|||
|
|||
> `columnList` is [linked list](../Common/Utils/Linked-List.md). You can use its methods to build a list of columns however you need. |
|||
@ -0,0 +1,108 @@ |
|||
# Entity Action Extensions for ASP.NET Core UI |
|||
|
|||
## Introduction |
|||
|
|||
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 JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md). |
|||
|
|||
### Create a JavaScript File |
|||
|
|||
First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: |
|||
|
|||
 |
|||
|
|||
Here, the content of this JavaScript file: |
|||
|
|||
```js |
|||
var clickMeAction = { |
|||
text: 'Click Me!', |
|||
action: function(data) { |
|||
//TODO: Write your custom code |
|||
alert(data.record.userName); |
|||
} |
|||
}; |
|||
|
|||
abp.ui.extensions.entityActions |
|||
.get('identity.user') |
|||
.addContributor(function(actionList) { |
|||
actionList.addTail(clickMeAction); |
|||
}); |
|||
``` |
|||
|
|||
In the `action` function, you can do anything you need. See the API section for a detailed usage. |
|||
|
|||
### Add the File to the User Management Page |
|||
|
|||
Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification System](Bundling-Minification.md). |
|||
|
|||
Write the following code inside the `ConfigureServices` of your module class: |
|||
|
|||
```csharp |
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options.ScriptBundles.Configure( |
|||
typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, |
|||
bundleConfiguration => |
|||
{ |
|||
bundleConfiguration.AddFiles( |
|||
"/Pages/Identity/Users/my-user-extensions.js" |
|||
); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. |
|||
|
|||
That's all. Run your application to see the result. |
|||
|
|||
## API |
|||
|
|||
This section explains details of the `abp.ui.extensions.entityActions` JavaScript API. |
|||
|
|||
### abp.ui.extensions.entityActions.get(entityName) |
|||
|
|||
This method is used to access the entity actions of a specific module. It takes one parameter: |
|||
|
|||
* **entityName**: The name of the entity defined by the related module. |
|||
|
|||
### abp.ui.extensions.entityActions.get(entityName).actions |
|||
|
|||
The `actions` property is used to retrieve a [doubly linked list](../Common/Utils/Linked-List.md) of previously defined actions for an entity. All contributors are executed in order to prepare the final actions list. This is normally called by the modules to show the actions in the grid. However, you can use it if you are building your own extensible UIs. |
|||
|
|||
### abp.ui.extensions.entityActions.get(entityName).addContributor(contributeCallback) |
|||
|
|||
The `addContributor` method covers all scenarios, e.g. you want to add your action in a different position in the list, change or remove an existing action item. `addContributor` with the following parameter: |
|||
|
|||
* **contributeCallback**: A callback function that is called whenever the action list should be created. You can freely modify the action list inside this callback method. |
|||
|
|||
#### Example |
|||
|
|||
```js |
|||
var clickMe2Action = { |
|||
text: 'Click Me 2!', |
|||
icon: 'fas fa-hand-point-right', |
|||
action: function(data) { |
|||
//TODO: Write your custom code |
|||
alert(data.record.userName); |
|||
} |
|||
}; |
|||
|
|||
abp.ui.extensions.entityActions |
|||
.get('identity.user') |
|||
.addContributor(function(actionList) { |
|||
// Remove an item from actionList |
|||
actionList.dropHead(); |
|||
|
|||
// Add the new item to the actionList |
|||
actionList.addHead(clickMe2Action); |
|||
}); |
|||
``` |
|||
|
|||
> `actionList` is [linked list](../Common/Utils/Linked-List.md). You can use its methods to build a list of columns however you need. |
|||
@ -1,3 +1,11 @@ |
|||
# Blazor UI: Authentication |
|||
|
|||
TODO |
|||
The [application startup template](../../Startup-Templates/Application.md) is properly configured to use OpenId Connect to authenticate the user through the server side login form; |
|||
|
|||
* When the Blazor application needs to authenticate, it is redirected to the server side. |
|||
* Users can enter username & password to login if they already have an account. If not, they can use the register form to create a new user. They can also use forgot password and other features. The server side uses IdentityServer4 to handle the authentication. |
|||
* Finally, they are redirected back to the Blazor application to complete the login process. |
|||
|
|||
This is a typical and recommended approach to implement authentication in Single-Page Applications. The client side configuration is done in the startup template, so you can change it. |
|||
|
|||
See the [Blazor Security document](https://docs.microsoft.com/en-us/aspnet/core/blazor/security) to understand and customize the authentication process. |
|||
@ -0,0 +1,75 @@ |
|||
# Blazor UI: Authorization |
|||
|
|||
Blazor applications can use the same authorization system and permissions defined in the server side. |
|||
|
|||
> This document is only for authorizing on the Blazor UI. See the [Server Side Authorization](../../Authorization.md) to learn how to define permissions and control the authorization system. |
|||
|
|||
## Basic Usage |
|||
|
|||
> ABP Framework is **100% compatible** with the Authorization infrastructure provided by the Blazor. See the [Blazor Security Document](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/) to learn all authorization options. This section **only shows some common scenarios**. |
|||
|
|||
### Authorize Attribute |
|||
|
|||
`[Authorize]` attribute can be used to show a page only to the authenticated users. |
|||
|
|||
````csharp |
|||
@page "/" |
|||
@attribute [Authorize] |
|||
|
|||
You can only see this if you're signed in. |
|||
```` |
|||
|
|||
The `[Authorize]` attribute also supports role-based or policy-based authorization. For example, you can check permissions defined in the server side: |
|||
|
|||
````csharp |
|||
@page "/" |
|||
@attribute [Authorize("MyPermission")] |
|||
|
|||
You can only see this if you have the necessary permission. |
|||
```` |
|||
|
|||
### AuthorizeView |
|||
|
|||
`AuthorizeView` component can be used in a page/component to conditionally render a part of the content: |
|||
|
|||
````html |
|||
<AuthorizeView Policy="MyPermission"> |
|||
<p>You can only see this if you satisfy the "MyPermission" policy.</p> |
|||
</AuthorizeView> |
|||
```` |
|||
|
|||
### IAuthorizationService |
|||
|
|||
`IAuthorizationService` can be injected and used to programmatically check permissions: |
|||
|
|||
````csharp |
|||
public partial class Index |
|||
{ |
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
if (await AuthorizationService.IsGrantedAsync("MyPermission")) |
|||
{ |
|||
//... |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
If your component directly or indirectly inherits from the `AbpComponentBase`, `AuthorizationService` becomes pre-injected and ready to use. If not, you can always [inject](../../Dependency-Injection.md) the `IAuthorizationService` yourself. |
|||
|
|||
`IAuthorizationService` can also be used in the view side where `AuthorizeView` component is not enough. |
|||
|
|||
There are some useful extension methods for the `IAuthorizationService`: |
|||
|
|||
* `IsGrantedAsync` simply returns `true` or `false` for the given policy/permission. |
|||
* `CheckAsync` checks and throws `AbpAuthorizationException` if given policy/permission hasn't granted. You don't have to handle these kind of exceptions since ABP Framework automatically [handles errors](Error-Handling.md). |
|||
* `AuthorizeAsync` returns `AuthorizationResult` as the standard way provided by the ASP.NET Core authorization system. |
|||
|
|||
> See the [Blazor Security Document](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/) to learn all authorization options |
|||
|
|||
## See Also |
|||
|
|||
* [Authorization](../../Authorization.md) (server side) |
|||
* [Blazor Security](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/) (Microsoft documentation) |
|||
* [ICurrentUser Service](CurrentUser.md) |
|||
|
|||
@ -0,0 +1,23 @@ |
|||
# Blazor UI: Current Tenant |
|||
|
|||
`ICurrentTenant` service can be used to get information about the current tenant in a [multi-tenant](../../Multi-Tenancy.md) application. `ICurrentTenant` defines the following properties; |
|||
|
|||
* `Id` (`Guid`): Id of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined. |
|||
* `Name` (`string`): Name of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined. |
|||
* `IsAvailable` (`bool`): Returns `true` if the `Id` is not `null`. |
|||
|
|||
**Example: Show the current tenant name on a page** |
|||
|
|||
````csharp |
|||
@page "/" |
|||
@using Volo.Abp.MultiTenancy |
|||
@inject ICurrentTenant CurrentTenant |
|||
@if (CurrentTenant.IsAvailable) |
|||
{ |
|||
<p>Current tenant name: @CurrentTenant.Name</p> |
|||
} |
|||
```` |
|||
|
|||
## See Also |
|||
|
|||
* [Multi-Tenancy](../../Multi-Tenancy.md) |
|||
@ -0,0 +1,22 @@ |
|||
# Blazor UI: Current User |
|||
|
|||
`ICurrentUser` service is used to obtain information about the currently authenticated user. Inject the `ICurrentUser` into any component/page and use its properties and methods. |
|||
|
|||
**Example: Show username & email on a page** |
|||
|
|||
````csharp |
|||
@page "/" |
|||
@using Volo.Abp.Users |
|||
@inject ICurrentUser CurrentUser |
|||
@if (CurrentUser.IsAuthenticated) |
|||
{ |
|||
<p>Welcome @CurrentUser.UserName</p> |
|||
} |
|||
```` |
|||
|
|||
> If you (directly or indirectly) derived your component from the `AbpComponentBase`, you can directly use the base `CurrentUser` property. |
|||
|
|||
`ICurrentUser` provides `Id`, `Name`, `SurName`, `Email`, `Roles` and some other properties. |
|||
|
|||
> See the [Server Side Current User](../../CurrentUser) service for more information. |
|||
|
|||
@ -0,0 +1,62 @@ |
|||
# Blazor UI: Error Handling |
|||
|
|||
Blazor, by default, shows a yellow line at the bottom of the page if any unhandled exception occurs. However, this is not useful in a real application. |
|||
|
|||
ABP provides an automatic error handling system for the Blazor UI. |
|||
|
|||
* Handles all unhandled exceptions and shows nice and useful messages to the user. |
|||
* It distinguishes different kind of exceptions. Hides internal/technical error details from the user (shows a generic error message in these cases). |
|||
* It is well integrated to the [server side exception handling](../../Exception-Handling.md) system. |
|||
|
|||
## Basic Usage |
|||
|
|||
There are different type of `Exception` classes handled differently by the ABP Framework. |
|||
|
|||
### UserFriendlyException |
|||
|
|||
`UserFriendlyException` is a special type of exception. You can directly show a error message dialog to the user by throwing such an exception. |
|||
|
|||
**Example** |
|||
|
|||
````csharp |
|||
@page "/" |
|||
@using Volo.Abp |
|||
|
|||
<Button Clicked="TestException">Throw test exception</Button> |
|||
|
|||
@code |
|||
{ |
|||
private void TestException() |
|||
{ |
|||
throw new UserFriendlyException("A user friendly error message!"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
ABP automatically handle the exception and show an error message to the user: |
|||
|
|||
 |
|||
|
|||
> You can derive from `UserFriendlyException` or directly implement `IUserFriendlyException` interface to create your own `Exception` class if you need. |
|||
|
|||
> You can use the [localization system](Localization.md) to show localized error messages. |
|||
|
|||
### BusinessException and Other Exception Types |
|||
|
|||
See the [exception handling document](../../Exception-Handling.md) to understand different kind of Exception class and interfaces and other capabilities of the Exception Handling system. |
|||
|
|||
## Generic Errors |
|||
|
|||
If the thrown `Exception` is not a special type, it is considered as generic error and a generic error message is shown to the user: |
|||
|
|||
 |
|||
|
|||
> All error details (including stack trace) are still written in the browser's console. |
|||
|
|||
## Server Side Errors |
|||
|
|||
Errors (like Validation, Authorization and User Friendly Errors) sent by the server are processed as you expect and properly shown to the user. So, error handling system works end to end without need to manually handle exceptions or manually transfer server-to-client error messages. |
|||
|
|||
## See Also |
|||
|
|||
* [Exception Handling System](../../Exception-Handling.md) |
|||
@ -0,0 +1,3 @@ |
|||
# Blazor UI: Page Header |
|||
|
|||
TODO |
|||
@ -0,0 +1,24 @@ |
|||
# Blazor UI: Routing |
|||
|
|||
Blazor has its own [routing system](https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing) and you can use it in your applications. ABP doesn't add any new feature to it, except one small improvement for the [modular development](../../Module-Development-Basics.md). |
|||
|
|||
## AbpRouterOptions |
|||
|
|||
Blazor `Router` component requires to define `AdditionalAssemblies` when you have components in assemblies/projects other than the main application's entrance assembly. So, if you want to create razor class libraries as ABP modules, you typically want to add the module's assembly to the `AdditionalAssemblies`. In this case, you need to add your module's assembly to the `AbpRouterOptions`. |
|||
|
|||
**Example** |
|||
|
|||
````csharp |
|||
Configure<AbpRouterOptions>(options => |
|||
{ |
|||
options.AdditionalAssemblies.Add(typeof(MyBlazorModule).Assembly); |
|||
}); |
|||
```` |
|||
|
|||
Write this code in the `ConfigureServices` method of your [module](../../Module-Development-Basics.md). |
|||
|
|||
`AbpRouterOptions` has another property, `AppAssembly`, which should be the entrance assembly of the application and typically set in the final application's module. If you've created your solution with the [application startup template](../../Startup-Templates/Application.md), it is already configured for you. |
|||
|
|||
## See Also |
|||
|
|||
* [Blazor Routing](https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing) (Microsoft Documentation) |
|||
@ -0,0 +1,3 @@ |
|||
# Blazor: Testing |
|||
|
|||
Coming soon. |
|||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 29 KiB |
@ -1,3 +1,3 @@ |
|||
@using Volo.Abp.Ui.Branding |
|||
@inject IBrandingProvider BrandingProvider |
|||
<a class="navbar-brand" href="/">@BrandingProvider.AppName</a> |
|||
<a class="navbar-brand" href="">@BrandingProvider.AppName</a> |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
namespace Volo.Abp.Authorization |
|||
{ |
|||
public static class AbpAuthorizationErrorCodes |
|||
{ |
|||
public const string GivenPolicyHasNotGranted = "Volo.Authorization:010001"; |
|||
|
|||
public const string GivenPolicyHasNotGrantedWithPolicyName = "Volo.Authorization:010002"; |
|||
|
|||
public const string GivenPolicyHasNotGrantedForGivenResource = "Volo.Authorization:010003"; |
|||
|
|||
public const string GivenRequirementHasNotGrantedForGivenResource = "Volo.Authorization:010004"; |
|||
|
|||
public const string GivenRequirementsHasNotGrantedForGivenResource = "Volo.Authorization:010005"; |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace Volo.Abp.Authorization.Localization |
|||
{ |
|||
[LocalizationResourceName("AbpAuthorization")] |
|||
public class AbpAuthorizationResource |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Volo.Authorization:010001": "Authorization failed! Given policy has not granted.", |
|||
"Volo.Authorization:010002": "Authorization failed! Given policy has not granted: {PolicyName}", |
|||
"Volo.Authorization:010003": "Authorization failed! Given policy has not granted for given resource: {ResourceName}", |
|||
"Volo.Authorization:010004": "Authorization failed! Given requirement has not granted for given resource: {ResourceName}", |
|||
"Volo.Authorization:010005": "Authorization failed! Given requirements has not granted for given resource: {ResourceName}" |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"culture": "tr", |
|||
"texts": { |
|||
"Volo.Authorization:010001": "Yetkilendirme başarısız! Belirtilen izin sağlanmamış.", |
|||
"Volo.Authorization:010002": "Yetkilendirme başarısız! Bu izin sağlanmamış: {PolicyName}", |
|||
"Volo.Authorization:010003": "Yetkilendirme başarısız! Bu izin, bu kaynak için sağlanmamış: {ResourceName}", |
|||
"Volo.Authorization:010004": "Yetkilendirme başarısız! Bu kaynak belirtilen gerekliliği sağlamamış: {ResourceName}", |
|||
"Volo.Authorization:010005": "Yetkilendirme başarısız! Bu kaynak belirtilen gereklilikleri sağlamamış: {ResourceName}" |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"Volo.Authorization:010001": "授权失败! 提供的策略尚未授予.", |
|||
"Volo.Authorization:010002": "授权失败! 提供的策略尚未授予: {PolicyName}", |
|||
"Volo.Authorization:010003": "授权失败! 提供的策略未授予提供的资源: {ResourceName}", |
|||
"Volo.Authorization:010004": "授权失败! 提供的要求未授予提供的资源: {ResourceName}", |
|||
"Volo.Authorization:010005": "授权失败! 提供的要求未授予提供的资源: {ResourceName}" |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Reflection; |
|||
using Autofac.Core.Activators.Reflection; |
|||
|
|||
namespace Volo.Abp.Autofac |
|||
{ |
|||
public class AbpAutofacConstructorFinder : IConstructorFinder |
|||
{ |
|||
private const BindingFlags DeclaredOnlyPublicFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; //Remove static constructor, BindingFlags.Static
|
|||
|
|||
private readonly Func<Type, ConstructorInfo[]> _finder; |
|||
|
|||
private static readonly ConcurrentDictionary<Type, ConstructorInfo[]> DefaultPublicConstructorsCache = new ConcurrentDictionary<Type, ConstructorInfo[]>(); |
|||
|
|||
public AbpAutofacConstructorFinder() |
|||
: this(GetDefaultPublicConstructors) |
|||
{ |
|||
} |
|||
|
|||
public AbpAutofacConstructorFinder(Func<Type, ConstructorInfo[]> finder) |
|||
{ |
|||
_finder = finder ?? throw new ArgumentNullException(nameof(finder)); |
|||
} |
|||
|
|||
public ConstructorInfo[] FindConstructors(Type targetType) |
|||
{ |
|||
return _finder(targetType); |
|||
} |
|||
|
|||
private static ConstructorInfo[] GetDefaultPublicConstructors(Type type) |
|||
{ |
|||
var retval = DefaultPublicConstructorsCache.GetOrAdd(type, t => t.GetConstructors(DeclaredOnlyPublicFlags)); |
|||
|
|||
if (retval.Length == 0) |
|||
{ |
|||
throw new NoConstructorsFoundException(type); |
|||
} |
|||
|
|||
return retval; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
<Button form="@Form" Type="@Type" Color="@Color" Block="@Block" PreventDefaultOnSubmit="@PreventDefaultOnSubmit" Disabled="@IsDisabled" Loading="@IsLoading" Clicked="@OnClickedHandler"> |
|||
@if ( ChildContent != null ) |
|||
{ |
|||
@ChildContent |
|||
} |
|||
else |
|||
{ |
|||
@SaveString |
|||
} |
|||
</Button> |
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Blazorise; |
|||
using Localization.Resources.AbpUi; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.Extensions.Localization; |
|||
|
|||
namespace Volo.Abp.BlazoriseUI.Components |
|||
{ |
|||
public partial class SubmitButton : ComponentBase |
|||
{ |
|||
protected bool Submiting { get; set; } |
|||
|
|||
[Parameter] |
|||
public string Form { get; set; } |
|||
|
|||
[Parameter] |
|||
public ButtonType Type { get; set; } = ButtonType.Submit; |
|||
|
|||
[Parameter] |
|||
public Color Color { get; set; } = Color.Primary; |
|||
|
|||
[Parameter] |
|||
public bool PreventDefaultOnSubmit { get; set; } = true; |
|||
|
|||
[Parameter] |
|||
public bool Block { get; set; } |
|||
|
|||
[Parameter] |
|||
public bool? Disabled { get; set; } |
|||
|
|||
[Parameter] |
|||
public string SaveResourceKey { get; set; } = "Save"; |
|||
|
|||
[Parameter] |
|||
public EventCallback Clicked { get; set; } |
|||
|
|||
[Parameter] |
|||
public RenderFragment ChildContent { get; set; } |
|||
|
|||
[Inject] |
|||
protected IStringLocalizer<AbpUiResource> StringLocalizer { get; set; } |
|||
|
|||
protected bool IsDisabled |
|||
=> Disabled == true || Submiting; |
|||
|
|||
protected bool IsLoading |
|||
=> Submiting; |
|||
|
|||
protected string SaveString |
|||
=> StringLocalizer[SaveResourceKey]; |
|||
|
|||
protected virtual async Task OnClickedHandler() |
|||
{ |
|||
try |
|||
{ |
|||
Submiting = true; |
|||
|
|||
await Clicked.InvokeAsync(null); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
throw; |
|||
} |
|||
finally |
|||
{ |
|||
Submiting = false; |
|||
|
|||
await InvokeAsync(StateHasChanged); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,10 +1,11 @@ |
|||
using Volo.Abp.Cli.ProjectBuilding.Building; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Cli.ProjectBuilding.Building; |
|||
|
|||
namespace Volo.Abp.Cli.ProjectBuilding |
|||
{ |
|||
public interface ITemplateInfoProvider |
|||
{ |
|||
TemplateInfo GetDefault(); |
|||
Task<TemplateInfo> GetDefaultAsync(); |
|||
|
|||
TemplateInfo Get(string name); |
|||
} |
|||
|
|||