@ -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 |