ASP.NET Zero Step by Step Development Guideline
How to create a new feature on ASP.NET Zero (Multi-tenant)
This guide includes steps as below
1- Setup Development Environment
2- Step by step development guideline
1- Setup Environment
Please refer to the original links:
- for Windows: https://docs.aspnetzero.com/en/aspnet-core-angular/latest/Getting-Started-Angular
- for macOS: https://docs.aspnetzero.com/en/aspnet-core-angular/latest/Getting-Started-MacOSX
2- Step by step development guideline
*We use the default login info as Default
tenancy name, username admin
, password 123qwe
(1) This guide starts with preconfigured multi-tenant support (MultiTenancyEnabled = true
) in file ./Core.Shared.AbpZeroTemplateConsts
public class AbpZeroTemplateConsts
{
public const string LocalizationSourceName = "AbpZeroTemplate";
public const string ConnectionStringName = "Default";
public const bool MultiTenancyEnabled = true;
public const bool AllowTenantsToChangeEmailSettings = false;
public const string Currency = "USD";
public const string CurrencySign = "$";
public const string AbpApiClientUserAgent = "AbpApiClient";
}
(2) Defining a Menu Item (on Angular
project)
(2.1) Open src/app/shared/layout/nav/app-navigation.service.ts in the client-side which defines menu items in the application. Create a new menu item as shown below
new AppMenuItem("Learning", null, "flaticon-book", "/app/main/learning")
Learning
is the menu name (will localize below), null
is for permission name (will set later), /app/main/learning
is the Angular route.
(2.2) Localize Menu Item Display Name
Go to .Core project
Add for default localization file .Core/Localization/AbpZeroTemplate/AbpZeroTemplate.xml
<text name="Learning">Learning</text>
Add for Vietnamese localization file AbpZeroTemplate-vi.xml
Note: Any change in server-side (including change localization texts) requires to recycle of the server application. You SHOULD use Ctrl + F5
if you don’t need to debug for a faster startup.
(2.3) Angular Route
open src/app/main/main-routing.module.ts in the client-side and add a new route
{ path: 'learning', component: LearningComponent }
Note: You must place your route above the “others” route. (path: ‘**’)
(3) Create LearningComponent (on Angular
project)
Create a learning folder inside src/app/main folder and add a new typescript file (learning.component.ts) in the student folder as shown below:
import { Component, Injector } from '@angular/core';
import { AppComponentBase } from '@shared/common/app-component-base';
import { appModuleAnimation } from '@shared/animations/routerTransition';@Component({
templateUrl: './learning.component.html',
animations: [appModuleAnimation()]
})export class LearningComponent extends AppComponentBase {
constructor(
injector: Injector
) {
super(injector);
}
}
We inherited from AppComponentBase
which provides some common functions and fields (like localization and access control). It’s not required, but it makes our job easier. Now we will import new component to main-routing.module.ts
import { LearningComponent } from './learning/learning.component';
Create a template for student component learning.component.html
<div [@routerTransition]>
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor">
<div class="kt-subheader kt-grid__item">
<div class="kt-container ">
<div class="kt-subheader__main">
<h3 class="kt-subheader__title">
<span>{{"Learning" | localize}}</span>
</h3>
</div>
</div>
</div>
<div class="kt-container kt-grid__item kt-grid__item--fluid">
<div class="kt-portlet kt-portlet--mobile">
<div class="kt-portlet__body kt-portlet__body--fit">
<p>STUDENT CONTENT COMES HERE!</p>
</div>
</div>
</div>
</div>
</div>
We use pipe
to perform localize. {{“Learning” | localize}}
[@routeTransition] attribute is required for page transition animation
And finally, we add this component to main.module.ts.
...
import { LearningComponent } from './learning/learning.component';
...
declarations: [
...
LearningComponent
]
(4) Create Student Entity
We must define entities in .Core project (server-side). This project defines Student entity (mapped to PbStudents table in database). We create a new folder Learning.
You should define constants as below
public class PersonConsts
{
public const int MaxNameLength = 32;
public const int MaxSurnameLength = 32;
public const int MaxEmailAddressLength = 255;
}public class PhoneConsts
{
public const int MaxNumberLength = 16;
}public class StudentConsts
{
public const int MaxNameLength = 32;
public const int MaxSurnameLength = 32;
public const int MaxEmailAddressLength = 255;
}
Add a DbSet property for Student entity to DbContext in project .EntityFrameworkCore
public virtual DbSet<Student> Students { get; set; }
(5) Database Migrations for Student
We use EntityFramework Core Code-First migration to migrate database schema. Since we added Student entity, our DbContext model is changed. So, we have to create a new migration to create the new table in database.
Go to .EntityFrameworkCore project in Terminal.
Run
dotnet ef migrations add "Added_Student_Table"
Check the generated file in .EntityFrameworkCore/Migrations
Run command to apply the new migration to Create new DbStudents table in Database
dotnet ef database update
Now we will seed initial data for this table
Add InitialStudentCreator.cs to ./EntityFrameworkCore/Migrations/Seed/Host
*Note: we set TenantId = 1
, for default admin
public class InitialStudentCreator
{
private readonly AbpZeroTemplateDbContext _context;public InitialStudentCreator(AbpZeroTemplateDbContext context)
{
_context = context;
}public void Create()
{
var douglas = _context.Students.FirstOrDefault(p => p.EmailAddress == "douglas.adams@fortytwo.com");
if (douglas == null)
{
_context.Students.Add(
new Student
{
Name = "Douglas",
Surname = "Adams",
EmailAddress = "douglas.adams@fortytwo.com",
TenantId = 1
});
}var asimov = _context.Students.FirstOrDefault(p => p.EmailAddress == "isaac.asimov@foundation.org");
if (asimov == null)
{
_context.Students.Add(
new Student
{
Name = "Isaac",
Surname = "Asimov",
EmailAddress = "isaac.asimov@foundation.org",
TenantId = 1
});
}
}
}
This default data is good because we can also use these data in Unit Test. We should be careful about seed data since this code will always be executed in each PostInitilize
(6) Create Unit Tests for Student Application Service
Create Student test case as follow
*We will create Unit Test before creating Services & Dtos
public class StudentAppService_Tests : AppTestBase
{
private readonly IStudentAppService _studentAppService; public StudentAppService_Tests()
{
_studentAppService = Resolve<IStudentAppService>();
} [Fact]
public void Should_Get_All_People_Without_Any_Filter()
{
//Act
var students = _studentAppService.GetPeople(new GetStudentInput()); //Assert
students.Items.Count.ShouldBe(2);
} [Fact]
public void Should_Get_Student_With_Filter()
{
//Act
var persons = _studentAppService.GetPeople(
new GetStudentInput
{
Filter = "Douglas"
}); //Assert
persons.Items.Count.ShouldBe(1);
persons.Items[0].Name.ShouldBe("Douglas");
persons.Items[0].Surname.ShouldBe("Adams");
} [Fact]
public async Task Should_Create_Student_With_Valid_Arguments()
{
//Act
await _studentAppService.CreatePerson(
new CreateStudentInput
{
Name = "John",
Surname = "Nash",
EmailAddress = "john.nash@abeautifulmind.com"
}); //Assert
UsingDbContext(
context =>
{
var john = context.Students.FirstOrDefault(p => p.EmailAddress == "john.nash@abeautifulmind.com");
john.ShouldNotBe(null);
john.Name.ShouldBe("John");
});
} [Fact]
public async Task Should_Not_Create_Student_With_Invalid_Arguments()
{
//Act and Assert
await Assert.ThrowsAsync<AbpValidationException>(
async () =>
{
await _studentAppService.CreatePerson(
new CreatePersonInput
{
Name = "John"
});
});
}
}
We derived test class from AppTestBase. AppTestBase class initializes all system, creates an in-memory fake database, seeds initial data (that we created before) to database and logins to application as admin. So, this is actually an integration test since it tests all server-side code from entity framework mapping to application services, validation and authorization.
(6.1) Create Service
An Application Service is used from client (Presentation Layer, Angular in this case) to perform operations (use cases) of the application.
Application Service interface and DTOs are located in .Application.Shared project.
- First, we create IStudentAppService
public interface IStudentAppService : IApplicationService
{
ListResultDto<StudentListDto> GetStudent(GetStudentInput input);
Task CreateStudent(CreateStudentInput input);
}
- Next, we create the required Dtos: StudentListDto.cs
public class StudentListDto : FullAuditedEntityDto
{
public string Name { get; set; } public string Surname { get; set; } public string EmailAddress { get; set; }
}
- GetStudentInput.cs
public class GetStudentInput
{
public string Filter { get; set; }
}
- GetStudentForEditInput.cs
public class GetStudentForEditInput
{
public int Id { get; set; }
}
- GetStudentForEditOutput.cs
public class GetStudentForEditOutput
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string EmailAddress { get; set; }
}
- CreateStudentInput.cs
public class CreateStudentInput
{
[Required]
[MaxLength(StudentConsts.MaxNameLength)]
public string Name { get; set; } [Required]
[MaxLength(StudentConsts.MaxSurnameLength)]
public string Surname { get; set; } [EmailAddress]
[MaxLength(StudentConsts.MaxEmailAddressLength)]
public string EmailAddress { get; set; }
}
Map Entity and Dto with CustomDtoMapper.cs in project .Application
configuration.CreateMap<Student, StudentListDto>();
configuration.CreateMap<CreateStudentInput, Student>();
configuration.CreateMap<Student, GetStudentForEditOutput>();
Now we will add the implementation of IStudentAppService to project .Application
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Abp.Collections.Extensions;
using Abp.Domain.Repositories;
using Abp.Extensions;
using Abp.Application.Services.Dto;
using MyCompanyName.AbpZeroTemplate.Learning.Dto;namespace MyCompanyName.AbpZeroTemplate.Learning
{
public class StudentAppService : AbpZeroTemplateAppServiceBase,
IStudentAppService
{
private readonly IRepository<Student> _studentRepository; public StudentAppService(IRepository<Student> studentRepository)
{
_studentRepository = studentRepository;
} public ListResultDto<StudentListDto> GetStudent(GetStudentInput input)
{
var students = _studentRepository
.GetAll()
.WhereIf(
!input.Filter.IsNullOrEmpty(),
p => p.Name.Contains(input.Filter) ||
p.Surname.Contains(input.Filter) ||
p.EmailAddress.Contains(input.Filter)
)
.OrderBy(p => p.Name)
.ThenBy(p => p.Surname)
.ToList();
return new ListResultDto<StudentListDto>(ObjectMapper.
Map<List<StudentListDto>>(students));
} public async Task CreateStudent(CreateStudentInput input)
{
var student = ObjectMapper.Map<Student>(input);
await _studentRepository.InsertAsync(student);
}
}
}
Here, we’re injecting student repository (it’s automatically created by ABP) and using it to filter and get student from database
WhereIf is an extension method here (defined in Abp.Linq.Extensions namespace). It performs Where condition, only if filter is not null or empty. IsNullOrEmpty is also an extension method (defined in Abp.Extensions namespace). ABP has many similar shortcut extension methods. ObjectMapper.Map method automatically converts list of Student entities to list of StudentListDto objects with using configurations in CustomDtoMapper.cs in .Application project.
Connection & Transaction Management
We don’t manually open database connection or start/commit transactions manually. It’s automatically done with ABP framework’s Unit Of Work system.
Exception Handling
We don’t handle exceptions manually (using a try-catch block). Because ABP framework automatically handles all exceptions on the web layer and returns appropriate error messages to the client. It then handles errors on the client and shows needed error information to the user.
(6.2) Ok. Now we get back to run Unit Test
(7) Using GetPeople Method From Angular Component
Now we will use the GetPeople method on the client site to display Student list.
Service Proxy Generation
First, run (prefer Ctrl+F5 to be faster) the server side application (.Web.Host project). Then run nswag/refresh.bat file on the client side to re-generate service proxy classes (they are used to call server side service methods).
*Should update config file to the port of back end.
Now run refresh command
cd nswag/
../node_modules/.bin/nswag run
Since we added a new service, we should add it to src/shared/service-proxies/service-proxy.module.ts. Just open it and add ApiServiceProxies.PersonServiceProxy to the providers array. This step is only required when we add a new service. If we change an existing service, it’s not needed.
Angular-Cli Watcher
Sometimes angular-cli can not understand the file changes. In that case, stop it and re-run npm start command.
LearningComponent Typescript Class
Update learning.component.ts
as below
import { Component, Injector, OnInit } from '@angular/core';
import { AppComponentBase } from '@shared/common/app-component-base';
import { appModuleAnimation } from '@shared/animations/routerTransition';
import { StudentServiceProxy, StudentListDto, ListResultDtoOfStudentListDto } from '@shared/service-proxies/service-proxies';@Component({
templateUrl: './learning.component.html',
animations: [appModuleAnimation()]
})export class LearningComponent extends AppComponentBase implements OnInit {
student: StudentListDto[] = [];
filter: string = ''; constructor(
injector: Injector,
private _studentService: StudentServiceProxy
) {
super(injector);
} ngOnInit(): void {
this.getPeople();
} getPeople(): void {
this._studentService.getStudent(this.filter).subscribe((result) => {
this.student = result.items;
});
}
}
We inject StudentServiceProxy, call its getStudent method and subscribe to get the result. We do this in ngOnInit function (defined in Angular’s OnInit interface). Assigned returned items to the student class member.
Rendering People In Angular View
Now, we can use this people member from the view, learning.component.html
<div [@routerTransition]>
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor">
<div class="kt-subheader kt-grid__item">
<div class="kt-container ">
<div class="kt-subheader__main">
<h3 class="kt-subheader__title">
<span>{{"Learning" | localize}}</span>
</h3>
</div>
</div>
</div>
<div class="kt-container kt-grid__item kt-grid__item--fluid">
<div class="kt-portlet kt-portlet--mobile">
<div class="kt-portlet__body kt-portlet__body--fit">
<h3>{{"AllStudent" | localize}}</h3>
<div *ngFor="let person of student">
<div class="row kt-row--no-padding align-items-center">
<div class="col">
<h4>{{person.name + ' ' + person.surname}}</h4>
<span>{{person.emailAddress}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
You can see the output as below
(8) Creating a New Person
create create-student-modal.component.ts file in ./angular/src/app/main/learning/ folder.
import { Component, ViewChild, Injector, ElementRef, Output, EventEmitter } from '@angular/core';
import { ModalDirective } from 'ngx-bootstrap';
import { StudentServiceProxy, CreateStudentInput } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/common/app-component-base';
import { finalize } from 'rxjs/operators';@Component({
selector: 'createStudentModal',
templateUrl: './create-student-modal.component.html'
})
export class CreateStudentModalComponent extends AppComponentBase { @Output() modalSave: EventEmitter<any> = new EventEmitter<any>(); @ViewChild('modal' , { static: false }) modal: ModalDirective;
@ViewChild('nameInput' , { static: false }) nameInput: ElementRef; student: CreateStudentInput = new CreateStudentInput(); active: boolean = false;
saving: boolean = false;constructor(
injector: Injector,
private _studentService: StudentServiceProxy
) {
super(injector);
} show(): void {
this.active = true;
this.student = new CreateStudentInput();
this.modal.show();
} onShown(): void {
this.nameInput.nativeElement.focus();
} save(): void {
this.saving = true;
this._studentService.createStudent(this.student)
.pipe(finalize(() => this.saving = false))
.subscribe(() => {
this.notify.info(this.l('SavedSuccessfully'));
this.close();
this.modalSave.emit(this.student);
});
} close(): void {
this.modal.hide();
this.active = false;
}
}
Let me explain some parts of this class:
- It has a selector, createStudentModal, which will be used as like an HTML element in the student list page.
- It extends AppComponentBase to take advantage of it (localize and access control).
- Defines an event, modalSave, which is triggered when we successfully save the modal. Thus, the main page will be informed and it can reload the student list.
- Declares two ViewChild members (modal and nameInput) to access some elements in the view.
- Injects StudentServiceProxy to call server side method while creating the student.
- It focuses to name input when modal is shown.
The code is simple and easy to understand except a small hack: an active flag is used to reset validation for Angular view (explained in angular’s documentation).
As declared in the component, we are creating the create-student-modal.component.html file in the same folder as shown below:
<div bsModal #modal="bs-modal" (onShown)="onShown()" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="modal" aria-hidden="true" [config]="{backdrop: 'static'}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form *ngIf="active" #studentForm="ngForm" novalidate (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title">
<span>{{"CreateNewStudent" | localize}}</span>
</h4>
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>{{"Name" | localize}}</label>
<input #nameInput class="form-control" type="text" name="name" [(ngModel)]="student.name" required maxlength="32">
</div>
<div class="form-group">
<label>{{"Surname" | localize}}</label>
<input class="form-control" type="text" name="surname" [(ngModel)]="student.surname" required maxlength="32">
</div>
<div class="form-group">
<label>{{"EmailAddress" | localize}}</label>
<input class="form-control" type="email" name="emailAddress" [(ngModel)]="student.emailAddress" required maxlength="255" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{1,})+$">
</div>
</div>
<div class="modal-footer">
<button [disabled]="saving" type="button" class="btn btn-secondary" (click)="close()">{{"Cancel" | localize}}</button>
<button type="submit" class="btn btn-primary" [disabled]="!personForm.form.valid" [buttonBusy]="saving" [busyText]="l('SavingWithThreeDot' | localize)"><i class="fa fa-save"></i> <span>{{"Save" | localize}}</span></button>
</div>
</form>
</div>
</div>
</div>
This modal is inside ngx-bootstrap library, you can refer here if you need more information.
Most of this code is similar for all modals. The important part is how we binded model to the view using the ngModel directive. As like all components, Angular requires to relate it to a module. We should add it to declarations array of main.module.ts as like shown below:
...previous imports
import { CreateStudentModalComponent } from './learning/create-student-modal.component';@NgModule({
imports: [
...existing module imports...
],
declarations: [
...
CreateStudentModalComponent
]
})
export class MainModule { }
We need to put a “Create new student” button to the ‘student list page’ to open the modal when clicked to the button. To do that, we made the following changes in learning.component.html:
<div [@routerTransition]>
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor">
<div class="kt-subheader kt-grid__item">
<div class="kt-container ">
<div class="kt-subheader__main">
<h3 class="kt-subheader__title">
<span>{{"Learning" | localize}}</span>
</h3>
<span class="kt-subheader__separator kt-subheader__separator--v"></span>
<span class="kt-subheader__desc">
{{"EditTenantHeaderInfo" | localize}}
</span>
</div>
<div class="kt-subheader__toolbar">
<div class="kt-subheader__wrapper">
<button class="btn btn-primary" (click)="createStudentModal.show()"><i class="fa fa-plus"></i>
{{"CreateNewStudent" | localize}}</button>
</div>
</div>
</div>
</div>
<div class="kt-container kt-grid__item kt-grid__item--fluid">
<div class="kt-portlet kt-portlet--mobile">
<div class="kt-portlet__body kt-portlet__body--fit">
<h3>{{"AllStudent" | localize}}</h3>
<div *ngFor="let person of student">
<div class="row kt-row--no-padding align-items-center">
<div class="col">
<h4>{{person.name + ' ' + person.surname}}</h4>
<span>{{person.emailAddress}}</span>
</div>
</div>
</div>
</div>
<createStudentModal #createStudentModal (modalSave)="getPeople()"></createStudentModal>
</div>
</div>
</div>
</div>
Made some minor changes in the view; Added a button to open the modal and the createStudentModal component as like another HTML tag (which matches to the selector in the create-student-modal.component.ts).
(9) Authorization for Learning Business
At this point, anyone can enter learning page since no authorization defined. We will define two permission:
- A permission to enter learning page.
- A permission to create new student (which is a child permission of first one, as naturally).
(9.1) Permission to enter learning page.
- Define the permission
Go to AppAuthorizationProvider class in the server side (./Core/Authorization) add a new permission as shown below (you can add just below the dashboard permission):
pages.CreateChildPermission(AppPermissions.Pages_Tenant_Learning, L("Learning"), multiTenancySides: MultiTenancySides.Tenant);
A permission should have a unique name. We define permission names as constant strings in AppPermissions class. It’s a simple constant string:
public const string Pages_Tenant_Learning = "Pages.Tenant.Learning";
Unique name of this permission is “Pages.Tenant.Learning”. While you can set any string (as long as it’s unique), it’s suggested to use that convention. A permission can have a localizable display name: “Learning” here. (See “Adding a New Page” section for more about localization, since it’s very similar). Lastly, we set this as a tenant level permission.
- Add AbpAuthorize attribute
AbpAuthorize attribute can be used as class level or method level to protect an application service or service method from unauthorized users. Since all server side code is located in StudentAppService
class, we can declare a class level attribute as shown below:
[AbpAuthorize(AppPermissions.Pages_Tenant_Student)]
public class StudentAppService : AbpZeroTemplateAppServiceBase,
IStudentAppService
{
//...
}
- Guard Angular Route
We got an exception about permission. Server did not send the data but we can still enter the page. To prevent it, open main-routing.module.ts and change the route definition like that:
{ path: 'learning', component: LearningComponent, data: { permission: 'Pages.Tenant.Learning' } }
AuthRouteGuard class automatically checks route permission data and prevents entering to the view if specified permission is not granted. Try to click Learning menu!
- Hide Unauthorized Menu Item
While user can not enter to the page, the menu item still there! We should also hide the Learning menu item. It’s easy, open ./src/app/shared/layout/nav/app-navigation-service.ts and add change Learning menu definition as shown below:
new AppMenuItem("Learning", 'Pages.Tenant.Learning', "flaticon-book", "/app/main/learning")
- Grant Permission
So, how we can enter the page now? We need to update permission for admin role. But because we are using in Multi-tenant mode and Learning feature is set at tenant level permission, so this permission will be applied for a tenant admin, not host admin.
Now, we have to create a tenant by this screen
You can log in tenant account by 2 ways as below
next, we will update the permissions of tenant admin.
We see that a new permission named “Learning” added to permissions tab. So, we can check it and save the role. After saving, we need to refresh the whole page to refresh permissions for the current user. We could also grant this permission to a specific user. Now, we can enter the Learning page again.
(9.2) Permission to create new student
While a permission for a page is useful and probably always needed, we may want to define additional permissions to perform some specific actions on a page, like creating a new student.
- Define a permission
Defining a permission is similar (in the ./Core/AuthorizationAppAuthorizationProvider
class):
// Remove old line
//pages.CreateChildPermission(AppPermissions.Pages_Tenant_Learning, L("Learning"), multiTenancySides: MultiTenancySides.Tenant);// New lines
var learning = pages.CreateChildPermission(AppPermissions.Pages_Tenant_Learning, L("Learning"), multiTenancySides: MultiTenancySides.Tenant); learning.CreateChildPermission(AppPermissions.Pages_Tenant_Learning_CreateStudent, L("CreateNewStudent"), multiTenancySides: MultiTenancySides.Tenant);
First permission was defined before. In the second line, we are creating a child permission of first one. Remember to create a constant in AppPermissions
class:
public const string Pages_Tenant_Learning_CreateStudent = "Pages.Tenant.Learning.CreateStudent";
- Add AbpAuthorize Attribute
This time, we’re declaring AbpAuthorize attribute just for CreateStudent method in Student in StudentAppService:
[AbpAuthorize(AppPermissions.Pages_Tenant_Learning_CreateStudent)]
public async Task CreateStudent(CreateStudentInput input)
{
// ..
}
- Hide Unauthorized Button
If we run the application and try to create a student, we get an authorization error after clicking the save button. But, it’s good to completely hide Create New Student button if we don’t have the permission. It’s very simple:
Open the learning.component.html view and add the permission Pages.Tenant.Learning.CreateStudent condition as shown below:
<button *ngIf="'Pages.Tenant.Learning.CreateStudent' | permission"class="btn btn-primary" (click)="createStudentModal.show()">
<i class="fa fa-plus"></i> {{"CreateNewStudent" | localize}}</button>
In this way, the “Create New Student” button is not rendered in server and user can not see this button.
- Grant Permission
To see the button again, we can go to role or user manager and grant related permission as shown below:
(10) Deleting a Student
- View
We’re changing learning.component.html view to add a delete button (related part is shown here):
...
<h3>{{"AllStudent" | localize}}</h3>
<div class="row kt-row--no-padding align-items-center" *ngFor="let person of student">
<div class="col">
<h4>{{person.name + ' ' + person.surname}}</h4>
<span>{{person.emailAddress}}</span>
</div>
<div class="col kt-align-right">
<button id="deleteStudent" (click)="deleteStudent(person)" title="{{'Delete' | localize}}"
class="btn btn-outline-hover-danger btn-icon"
href="javascript:;">
<i class="fa fa-times"></i>
</button>
</div>
</div>
...
- Style
We’re using LESS files for styling the components. We created a file named learning.component.less (in learning folder) with an empty content.
/* styles */
And adding the style to the learning.component.ts Component declaration:
@Component({
templateUrl: './learning.component.html',
styleUrls: ['./learning.component.less'],
animations: [appModuleAnimation()]
})
Now, we can now see the buttons, but they don’t work since we haven’t defined the deletePerson method yet.
- Application Service
Let’s leave the client side and add a DeleteStudent method to the server side. We are adding it to the service interface,IStudentAppService:
Task DeleteStudent(EntityDto input);
EntityDto is a shortcut of ABP if we only get an id value. Implementation (in StudentAppService) is very simple:
[AbpAuthorize(AppPermissions.Pages_Tenant_Learning_DeleteStudent)]
public async Task DeleteStudent(EntityDto input)
{
await _studentRepository.DeleteAsync(input.Id);
}
add constant Pages_Tenant_Learning_DeleteStudent to AppPermissions.
public const string Pages_Tenant_Learning_DeleteStudent = "Pages.Tenant.Learning.DeleteStudent";
add permission to AppAuthorizationProvider
learning.CreateChildPermission(AppPermissions.Pages_Tenant_Learning_CreateStudent, L("DeleteStudent"), multiTenancySides: MultiTenancySides.Tenant);
- Service Proxy Generation
Since we changed server side services, we should re-generate the client side service proxies via NSwag. Make server side running and use refresh.bat as we did before.
- Learning Component Script
// import lodash library
import * as _ from 'lodash';...// add code for delete method
deleteStudent(student: StudentListDto): void {
this.message.confirm(
this.l('AreYouSureToDeleteTheStudent', student.name),
student.name,
isConfirmed => {
if (isConfirmed) {
this._studentService.deleteStudent(student.id).subscribe(() => {
this.notify.info(this.l('SuccessfullyDeleted'));
_.remove(this.student, student);
});
}
}
);
}
It first shows a confirmation message when we click the delete button:
If we click Yes, it simply calls deletePerson method of PersonAppService and shows a notification if operation succeed. Also, removes the person from the person array using lodash library.
(11) Filtering Student
We added a search input to learning.component.html view (showing the related part of the code):
<h3>{{"AllStudent" | localize}} ({{student.length}})</h3>
<form autocomplete="off">
<div class="kt-form">
<div class="row align-items-center kt-margin-b-10">
<div class="col-xl-12">
<div class="form-group align-items-center">
<div class="input-group">
<input [(ngModel)]="filter" name="filterText" autoFocus class="form-control" [placeholder]="l('SearchWithThreeDot' | localize)" type="text">
<span class="input-group-btn">
<button (click)="getPeople()" class="btn btn-primary" type="submit"><i class="flaticon-search-1"></i></button>
</span>
</div>
</div>
</div>
</div>
</div>
</form>...
<div class="row kt-row--no-padding align-items-center" *ngFor="let person of student">
<div class="col">
<h4>{{person.name + ' ' + person.surname}}</h4>
<span>{{person.emailAddress}}</span>
</div>
<div class="col kt-align-right">
<button id="deleteStudent" (click)="deleteStudent(person)" title="{{'Delete' | localize}}"
class="btn btn-outline-hover-danger btn-icon"
href="javascript:;">
<i class="fa fa-times"></i>
</button>
</div>
</div>
We also added currently filtered student count (student.length) in the header. Since we have already defined and used the filter property in learning.component.ts and implemented in the server side, this new code immediately works.
(12) Edit Mode For People
First of all, we create the necessary DTOs to transfer student’s id, name, surname and e-mail. We can optionally configure auto-mapper, but this is not necessary because all properties match automatically. Then we create the functions in StudentAppService for editing people:
StudentAppService
[AbpAuthorize(AppPermissions.Pages_Tenant_Learning_EditStudent)]
public async Task<GetStudentForEditOutput> GetStudentForEdit(GetStudentForEditInput input)
{
var student = await _studentRepository.GetAsync(input.Id);
return ObjectMapper.Map<GetStudentForEditOutput>(student);
}[AbpAuthorize(AppPermissions.Pages_Tenant_Learning_EditStudent)]
public async Task EditStudent(EditStudentInput input)
{
var student = await _studentRepository.GetAsync(input.Id);
student.Name = input.Name;
student.Surname = input.Surname;
student.EmailAddress = input.EmailAddress;
await _studentRepository.UpdateAsync(student);
}
IStudentAppService
Task<GetStudentForEditOutput> GetStudentForEdit(GetStudentForEditInput input);
Task EditStudent(EditStudentInput input);
Create new EditStudentInput class in ./Application/Shared/Learning/Dto
public class EditStudentInput
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string EmailAddress { get; set; }
}
Create new GetStudentForEditInput
public int Id { get; set; }
Create new GetStudentForEditOutput
public class GetStudentForEditOutput
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string EmailAddress { get; set; }
}
Then we add configuration for AutoMapper into CustomDtoMapper.cs like below:
configuration.CreateMap<Student, GetStudentForEditOutput>();
Add to AppPermissions
public const string Pages_Tenant_Learning_EditStudent = "Pages.Tenant.Learning.EditStudent";
Add to AppAuthorizationProvider
learning.CreateChildPermission(AppPermissions.Pages_Tenant_Learning_EditStudent, L("EditStudent"), multiTenancySides: MultiTenancySides.Tenant);
- Service Proxy Generation
Since we changed server side services, we should re-generate the client side service proxies via NSwag. Make server side running and use refresh.bat as we did before.
- View
Create edit-student-modal.component.html:
<div bsModal #modal="bs-modal" (onShown)="onShown()" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="modal" aria-hidden="true" [config]="{backdrop: 'static'}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form *ngIf="active" #studentForm="ngForm" novalidate (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title">
<span>{{"EditStudent" | localize}}</span>
</h4>
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>{{"Name" | localize}}</label>
<input #nameInput class="form-control" type="text" name="name" [(ngModel)]="student.name" required maxlength="32">
</div>
<div class="form-group">
<label>{{"Surname" | localize}}</label>
<input class="form-control" type="text" name="surname" [(ngModel)]="student.surname" required maxlength="32">
</div>
<div class="form-group">
<label>{{"EmailAddress" | localize}}</label>
<input class="form-control" type="email" name="emailAddress" [(ngModel)]="student.emailAddress" required maxlength="255" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{1,})+$">
</div>
</div>
<div class="modal-footer">
<button [disabled]="saving" type="button" class="btn btn-secondary" (click)="close()">{{"Cancel" | localize}}</button>
<button type="submit" class="btn btn-primary" [disabled]="!studentForm.form.valid" [buttonBusy]="saving" [busyText]="l('SavingWithThreeDot' | localize)"><i class="fa fa-save"></i> <span>{{"Save" | localize}}</span></button>
</div>
</form>
</div>
</div>
</div>
Add those lines to learning.component.html:
...
<button *ngIf="'Pages.Tenant.Learning.EditStudent' | permission"
(click)="editStudentModal.show(person.id)" title="{{'EditStudent' | localize}}"
class="btn btn-outline-hover-success btn-icon">
<i class="fa fa-edit"></i>
</button>
<button id="deleteStudent" (click)="deleteStudent(person)" title="{{'Delete' | localize}}"
class="btn btn-outline-hover-danger btn-icon"
href="javascript:;">
<i class="fa fa-times"></i>
</button>
...<createStudentModal #createStudentModal (modalSave)="getPeople()"></createStudentModal>
<editStudentModal #editStudentModal (modalSave)="getPeople()"></editStudentModal>
- Controller
Create edit-student-modal.component.ts:
import { Component, ViewChild, Injector, ElementRef, Output, EventEmitter } from '@angular/core';
import { ModalDirective } from 'ngx-bootstrap';
import { StudentServiceProxy, EditStudentInput } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/common/app-component-base';@Component({
selector: 'editStudentModal',
templateUrl: './edit-student-modal.component.html'
})
export class EditStudentModalComponent extends AppComponentBase {@Output() modalSave: EventEmitter<any> = new EventEmitter<any>();@ViewChild('modal' , { static: false }) modal: ModalDirective;
@ViewChild('nameInput' , { static: false }) nameInput: ElementRef;student: EditStudentInput = new EditStudentInput();active: boolean = false;
saving: boolean = false;constructor(
injector: Injector,
private _studentService: StudentServiceProxy
) {
super(injector);
}show(studentId): void {
this.active = true;
this._studentService.getStudentForEdit(studentId).subscribe((result)=> {
this.student = result;
this.modal.show();
});}onShown(): void {
// this.nameInput.nativeElement.focus();
}save(): void {
this.saving = true;
this._studentService.editPerson(this.student)
.subscribe(() => {
this.notify.info(this.l('SavedSuccessfully'));
this.close();
this.modalSave.emit(this.student);
});
this.saving = false;
}close(): void {
this.modal.hide();
this.active = false;
}
}
Add those lines to main.module.ts:
import { EditStudentModalComponent } from './learning/edit-student-modal.component';// Other Code lines...declarations: [
...
CreateStudentModalComponent,
EditStudentModalComponent
]// Other Code lines...
That’s it! All done! And this is our result