Skip to content

Commit b21f214

Browse files
committed
Update DevExtreme Angular blog post
1 parent e5deeef commit b21f214

6 files changed

+1245
-1282
lines changed
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# Using ASP.NET Zero with DevExtreme Angular Part 1
2+
3+
In this tutorial, we will guide you through the process of creating a robust and **feature-rich** phonebook application using ASP.NET Zero, which is built on ASP.NET Core and Angular. By following the steps outlined here, you will learn how to develop a **multi-tenant** application that supports **localization**, **authorization**, **configurability**, and **testability**.
4+
5+
ASP.NET Zero provides a solid foundation for building **enterprise-level** applications with its **comprehensive** set of **pre-built features** and functionalities. By leveraging the power of ASP.NET Core on the server-side and Angular on the client-side, you can create a **modern** and **responsive** web application.
6+
7+
By the end of this tutorial, you will have a solid understanding of how to use ASP.NET Zero Angular and DevExtreme together to develop **powerful** applications that are both **user-friendly** and highly functional. So, let's dive in and start building our phonebook application step by step.
8+
9+
## Creating & Running The Project
10+
11+
We're creating and downloading the solution named `Acme.PhoneBookDemo` as described in [Getting Started]([Getting-Started-Angular](https://docs.aspnetzero.com/en/aspnet-core-angular/latest/Getting-Started-Angular)) document. Please follow the getting started document, run the application, login as default tenant admin (select `Default` as tenancy name, use `admin` as username and `123qwe` as the password) and see the dashboard below:
12+
13+
<img src="/Images/Blog/dashboard1.png" alt="Dashboard" style="width:100.0%" />
14+
15+
Logout from the application for now. We will make our application **single-tenant** (we will convert it to multi-tenant later). So, we open `PhoneBookDemoConsts` class in the `Acme.PhoneBookDemo.Core.Shared` project and disable multi-tenancy as shown below:
16+
17+
```c#
18+
public class PhoneBookDemoConsts
19+
{
20+
public const string LocalizationSourceName = "PhoneBookDemo";
21+
22+
public const string ConnectionStringName = "Default";
23+
24+
public const bool MultiTenancyEnabled = false;
25+
26+
public const int PaymentCacheDurationInMinutes = 30;
27+
}
28+
```
29+
30+
**Note:** If you log in before changing **MultiTenancyEnabled** to false, you might be get login error. To overcome this, you should remove cookies.
31+
32+
## Adding a New Menu Item
33+
34+
Let's begin from UI and create a new page named "**Phone book**".
35+
36+
### Defining a Menu Item
37+
38+
Open `*\src\app\shared\layout\nav\app-navigation.service.ts` in the client side (`Acme.PhoneBookDemo.AngularUI`) which defines menu items in the application. Create new menu item as shown below (You can add it right after the dashboard menu item).
39+
40+
```csharp
41+
new AppMenuItem("PhoneBook", null, "flaticon-book", "/app/main/phonebook")
42+
```
43+
44+
`PhoneBook` is the menu name (will localize below), `null` is for permission name (will set it later), `flaticon-book` is just an arbitrary icon class (from [this set](http://keenthemes.com/metronic/preview/?page=components/icons/flaticon&demo=default)) and `/phonebook` is the Angular route.
45+
46+
If you run the application, you can see a new menu item on the left menu, but it won't work (it redirects to default route) if you click to the menu item, since we haven't defined the Angular route yet.
47+
48+
### Localize Menu Item Display Name
49+
50+
Localization strings are defined in **XML** files in `.Core` project in server side as shown below:
51+
52+
<img src="/Images/Blog/localization-files-4.png" alt="Localization files" class="img-thumbnail" />
53+
54+
Open `PhoneBookDemo.xml` (the **default**, **English** localization dictionary) and add the following line:
55+
56+
```xml
57+
<text name="PhoneBook">Phone Book</text>
58+
```
59+
60+
If we don't define "PhoneBook"s value for other localization dictionaries, default value is shown in all languages. For example, we can define it also for Turkish in `PhoneBookDmo-tr.xml` file:
61+
62+
```xml
63+
<text name="PhoneBook">Telefon Rehberi</text>
64+
```
65+
66+
**Note:** Any change in server side (including change localization texts) requires restart of the server application. We suggest to use **Ctrl+F5** if you don't need to debugging for a faster startup. In that case, it's enough to make a **re-build** to recycle the application.
67+
68+
### Angular Route
69+
70+
Angular has a powerful URL routing system. ASP.NET Zero has defined routes in a few places (for modularity, see [main menu & layout]([Features-Angular-Main-Menu-Layout.md](https://docs.aspnetzero.com/en/aspnet-core-angular/latest/Features-Angular-Main-Menu-Layout))). We want to add phone book page to the main module. So, open `*\src\app\main\main-routing.module.ts` in the client side and add a new route just below to the dashboard:
71+
72+
```ts
73+
{
74+
path: 'phonebook',
75+
loadChildren: () => import('./phonebook/phonebook.module').then(m => m.PhonebookModule)
76+
}
77+
```
78+
79+
We get an error since we haven't defined `PhoneBookModule` yet. Also, we ignored permission for now (will implement later).
80+
81+
## Creating the PhoneBook Component
82+
83+
Create a `phonebook` folder inside `*\src\app\main` folder and add a
84+
new typescript file `phonebook.component.ts` as shown below:
85+
86+
```javascript
87+
import { Component, Injector } from '@angular/core';
88+
import { AppComponentBase } from '@shared/common/app-component-base';
89+
import { appModuleAnimation } from '@shared/animations/routerTransition';
90+
91+
@Component({
92+
templateUrl: './phonebook.component.html',
93+
animations: [appModuleAnimation()]
94+
})
95+
96+
export class PhoneBookComponent extends AppComponentBase {
97+
constructor(
98+
injector: Injector
99+
) {
100+
super(injector);
101+
}
102+
}
103+
```
104+
105+
We inherited from `AppComponentBase` which provides some common functions and fields (like localization and access control) for us. It's not required, but makes our job easier.
106+
107+
As we declared in `phonebook.component.ts` we should create a
108+
`phonebook.component.html` view in the same folder.
109+
110+
```html
111+
<div [@routerTransition]>
112+
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor">
113+
<div class="kt-subheader kt-grid__item">
114+
<div [class]="containerClass">
115+
<div class="kt-subheader__main">
116+
<h3 class="kt-subheader__title">
117+
<span>{{"PhoneBook" | localize}}</span>
118+
</h3>
119+
</div>
120+
</div>
121+
</div>
122+
<div [class]="containerClass + ' kt-grid__item kt-grid__item--fluid'"
123+
<div class="kt-portlet kt-portlet--mobile">
124+
<div class="kt-portlet__body kt-portlet__body--fit">
125+
<p>PHONE BOOK CONTENT COMES HERE!</p>
126+
</div>
127+
</div>
128+
</div>
129+
</div>
130+
</div>
131+
```
132+
133+
`l` **(lower case 'L')** function comes from `AppComponentBase` and used to easily localize texts. `@routerTransition` attribute is
134+
required for page transition animation.
135+
136+
Now we should create a `phonebook.module.ts` and `phonebook-routing.module.ts` view in the same folder:
137+
138+
*phonebook-routing.module.ts*
139+
140+
```typescript
141+
import {NgModule} from '@angular/core';
142+
import {RouterModule, Routes} from '@angular/router';
143+
import {PhoneBookComponent} from './phonebook.component';
144+
145+
const routes: Routes = [{
146+
path: '',
147+
component: PhoneBookComponent,
148+
pathMatch: 'full'
149+
}];
150+
151+
@NgModule({
152+
imports: [RouterModule.forChild(routes)],
153+
exports: [RouterModule],
154+
})
155+
export class PhoneBookRoutingModule {}
156+
```
157+
158+
*phonebook.module.ts*
159+
160+
```typescript
161+
import {NgModule} from '@angular/core';
162+
import {AppSharedModule} from '@app/shared/app-shared.module';
163+
import {PhoneBookRoutingModule} from './phonebook-routing.module';
164+
import {PhoneBookComponent} from './phonebook.component';
165+
166+
@NgModule({
167+
declarations: [PhoneBookComponent],
168+
imports: [AppSharedModule, PhoneBookRoutingModule]
169+
})
170+
export class PhoneBookModule {}
171+
```
172+
173+
Now, we can refresh the page to see the new added page:
174+
175+
<img src="/Images/Blog/phonebook-empty-ng2.png" alt="Phonebook empty" class="img-thumbnail" style="width:100.0%" />
176+
177+
**Note:** Angular-cli automatically re-compiles and refreshes the page when any changes made to any file in the application.
178+
179+
## Creating Person Entity
180+
181+
We define entities in `.Core` (domain) project (in server side). We
182+
can define a `Person` entity (mapped to `PbPersons` table in
183+
database) to represent a person in phone book as shown below (I created in a new folder/namespace named `PhoneBook`):
184+
185+
```csharp
186+
using System.ComponentModel.DataAnnotations;
187+
using System.ComponentModel.DataAnnotations.Schema;
188+
using Abp.Domain.Entities.Auditing;
189+
190+
namespace Acme.PhoneBookDemo.PhoneBook
191+
{
192+
[Table("PbPersons")]
193+
public class Person : FullAuditedEntity
194+
{
195+
public const int MaxNameLength = 32;
196+
public const int MaxSurnameLength = 32;
197+
public const int MaxEmailAddressLength = 255;
198+
199+
[Required]
200+
[MaxLength(MaxNameLength)]
201+
public virtual string Name { get; set; }
202+
203+
[Required]
204+
[MaxLength(MaxSurnameLength)]
205+
public virtual string Surname { get; set; }
206+
207+
[MaxLength(MaxEmailAddressLength)]
208+
public virtual string EmailAddress { get; set; }
209+
}
210+
}
211+
```
212+
213+
Person's **primary key** type is **int** (as default). It inherits `FullAuditedEntity` that contains `creation`, `modification` and `deletion` audit properties. It's also `soft-delete`. When we delete a person, it's not deleted by database but marked as deleted (see [entity](https://aspnetboilerplate.com/Pages/Documents/Entities) and [data filters](https://aspnetboilerplate.com/Pages/Documents/Data-Filters) documentations for more information). We created consts for `MaxLength` properties. This is a good practice since we will use same values later.
214+
215+
We add a `DbSet` property for `Person` entity to `PhoneBookDemoDbContext` class defined in `.EntityFrameworkCore` project.
216+
217+
```csharp
218+
public class PhoneBookDemoDbContext : AbpZeroDbContext<Tenant, Role, User, PhoneBookDemoDbContext>
219+
{
220+
public virtual DbSet<Person> Persons { get; set; }
221+
222+
//...other code
223+
}
224+
```
225+
226+
## Database Migrations for Person
227+
228+
We use **EntityFramework Code-First migrations** to migrate database schema. Since we added `Person` entity, our DbContext model is changed. So, we should create a **new migration** to create the new table in the database.
229+
230+
### Creating Migration
231+
232+
Open **Package Manager Console**, run the `Add-Migration "Added_Persons_Table"` command as shown below:
233+
234+
<img src="/Images/Blog/phonebook-migrations-core-3.png" alt="Entity Framework Code First Migration" class="img-thumbnail" />
235+
236+
This command will add a **migration class** named `Added_Persons_Table` as shown below:
237+
238+
```csharp
239+
public partial class Added_Persons_Table : Migration
240+
{
241+
protected override void Up(MigrationBuilder migrationBuilder)
242+
{
243+
migrationBuilder.CreateTable(
244+
name: "PbPersons",
245+
columns: table => new
246+
{
247+
Id = table.Column(nullable: false)
248+
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
249+
CreationTime = table.Column(nullable: false),
250+
CreatorUserId = table.Column(nullable: true),
251+
DeleterUserId = table.Column(nullable: true),
252+
DeletionTime = table.Column(nullable: true),
253+
EmailAddress = table.Column(maxLength: 255, nullable: true),
254+
IsDeleted = table.Column(nullable: false),
255+
LastModificationTime = table.Column(nullable: true),
256+
LastModifierUserId = table.Column(nullable: true),
257+
Name = table.Column(maxLength: 32, nullable: false),
258+
Surname = table.Column(maxLength: 32, nullable: false)
259+
},
260+
constraints: table =>
261+
{
262+
table.PrimaryKey("PK_PbPersons", x => x.Id);
263+
});
264+
}
265+
266+
protected override void Down(MigrationBuilder migrationBuilder)
267+
{
268+
migrationBuilder.DropTable(
269+
name: "PbPersons");
270+
}
271+
}
272+
```
273+
274+
We don't have to know so much about format and rules of this file. But, it's suggested to have a basic understanding of migrations. In the same **Package Manager Console**, we write `Update-Database` command in order to apply the new migration to database. After updating, we can see that `PbPersons` table is added to database.
275+
276+
<img src="/Images/Blog/phonebook-tables-spa.png" alt="Phonebook tables" class="img-thumbnail" />
277+
278+
But this new table is empty. In ASP.NET Zero, there are some classes to fill initial data for users and settings:
279+
280+
<img src="/Images/Blog/aspnet-core-ef-seed-1.png" alt="Seed folders" class="img-thumbnail" />
281+
282+
### Seed Person Data
283+
284+
So, we can add a separated class to fill some people to database as shown below:
285+
286+
```csharp
287+
namespace Acme.PhoneBookDemo.Migrations.Seed.Host
288+
{
289+
public class InitialPeopleCreator
290+
{
291+
private readonly PhoneBookDemoDbContext _context;
292+
293+
public InitialPeopleCreator(PhoneBookDemoDbContext context)
294+
{
295+
_context = context;
296+
}
297+
298+
public void Create()
299+
{
300+
var douglas = _context.Persons.FirstOrDefault(p => p.EmailAddress == "[email protected]");
301+
if (douglas == null)
302+
{
303+
_context.Persons.Add(
304+
new Person
305+
{
306+
Name = "Douglas",
307+
Surname = "Adams",
308+
EmailAddress = "[email protected]"
309+
});
310+
}
311+
312+
var asimov = _context.Persons.FirstOrDefault(p => p.EmailAddress == "[email protected]");
313+
if (asimov == null)
314+
{
315+
_context.Persons.Add(
316+
new Person
317+
{
318+
Name = "Isaac",
319+
Surname = "Asimov",
320+
EmailAddress = "[email protected]"
321+
});
322+
}
323+
}
324+
}
325+
}
326+
```
327+
328+
These type of default data is good since we can also use these data in **unit tests**. Surely, we should be careful about seed data since this code will always be executed in each `PostInitialize` of your `PhoneBookEntityFrameworkCoreModule`. This class (`InitialPeopleCreator`) is created and called in `InitialHostDbBuilder` class. This is not so important, just for a good code organization (see source codes).
329+
330+
```csharp
331+
public class InitialHostDbBuilder
332+
{
333+
//existing codes...
334+
335+
public void Create()
336+
{
337+
//existing code...
338+
new InitialPeopleCreator(_context).Create();
339+
340+
_context.SaveChanges();
341+
}
342+
}
343+
```
344+
345+
We run our project again, it runs seed and adds two people to `PbPersons` table:
346+
347+
<img src="/Images/Blog/phonebook-persons-table-initial-data.png" alt="Persons initial data" class="img-thumbnail" width="720" height="50" />
348+
349+
## Conclusion
350+
351+
In this tutorial, we have explored the process of creating a phonebook application using ASP.NET Zero, which is built on ASP.NET Core and Angular, and integrated with DevExtreme components.
352+
353+
### Summary
354+
355+
* We started by creating and setting up the ASP.NET Zero solution, following the instructions in the Getting Started document. We logged in as the default tenant admin and familiarized ourselves with the dashboard.
356+
357+
* We made the application **single-tenant** by disabling multi-tenancy in the `PhoneBookDemoConsts` class.
358+
359+
* Next, we added a new menu item for the phonebook page by defining a menu item in the `AppNavigationService` class. We also localized the menu item display name by adding the necessary entries in the localization XML files.
360+
361+
* We defined the Angular route for the phonebook page by adding a new route in the `main-routing.module.ts` file. We created the corresponding components and modules for the phonebook page and implemented a basic layout and content.
362+
363+
* Moving to the server-side, we created a `Person` entity to represent a person in the phonebook and added it to the `PhoneBookDemoDbContext`.
364+
365+
* We used Entity Framework **Code-First** Migrations to create a new migration for the `Person` entity and applied it to the database. This created the `PbPersons` table in the database.
366+
367+
* Finally, we added seed data to the `Person` table using the `InitialPeopleCreator` class, which was called in the `InitialHostDbBuilder`. This allowed us to have some initial data in the database.
368+
369+
### What's Next?
370+
371+
In the next tutorial, we will add a **application service** to manage the phonebook. We will also add a **unit test** for the application service. Finally, we will add a **new page** to the **Angular** application to display the phonebook.
372+
373+
https://aspnetzero.com/blog/using-asp.net-zero-with-devextreme-angular-part-2

0 commit comments

Comments
 (0)