TRUNGTQ

Think Big, Act Small, Fail Fast and Learn Rapidly

NAVIGATION - SEARCH

BroadcastReceiver

Tạp chí Lập trình – Khi trả lời câu hỏi “Lập trình mobile khác gì với lập trình trên nền tảng PC?”, thì có đến khoảng 90% các bạn lập trình viên sẽ nghĩ ngay đến 2 vấn đề “Memory’ – Bộ nhớ và “Performance’ – Tốc độ xử lí. Điều này rất dễ giải thích vì các bạn đều biết mobile là một thiết bị với giới hạn về tốc độ xử lí của chip (CPU) và dung lượng bộ nhớ trong (RAM).

Nhưng trong bài viết này tôi sẽ đề cập đến một vấn đề khác cũng không kém phần quan trong trọng và thực sự là khác biệt của lập trình mobile. Đã có rất ít bạn khi mới làm quen với lập trình mobile để ý. Đó là vấn đề “Competition” – cạnh tranh. Để có một ứng dụng tốt thì nhất định phải giải quyết được cả 3 vấn đề trên.

Vậy Competition trong lập trình mobile thể hiện như thế nào? Tôi sẽ lấy các ví dụ và cách giải quyết bài toán trong Android để giải thích khái niệm này.

Vì trong một bài viết không thể nói hết được một vấn đề lớn của lập trinh mobile nên tôi chỉ mong có thể giải thích khái niệm và cơ chế hoạt động chứ không quá đi sâu chi tiết về coding.

TH1 : Các bạn đang nghe nhạc thì có một cuộc gọi đến. Nhạc sẽ tắt để bạn có thể trả lời cuộc gọi. Sau khi cuộc gọi kết thúc nhạc sẽ lại tự động được bật lên.

Trong tình huống này ứng dụng nghe nhạc đã phải tạm dừng để ưu tiên cho ứng dụng gọi điện được hoạt động. Khi kết thúc cuộc gọi thì ứng dụng nghe nhạc sẽ lại được hoạt động trở lại. Điều này không phải do hệ thống tự xử lý mà được xử lý trong ứng dụng nghe nhạc. Nếu bạn muốn tạo ra ứng dụng nghe nhạc của riêng mình thì đây là một điều cần chú ý nếu không người dùng sẽ vừa phải nghe nhạc và vừa nghe điện thoại J

Tương tự với các game, nếu bạn đang chơi game mà có cuộc gọi đến thì game của bạn sẽ phải tạm dừng, sau khi kết thúc cuộc gọi màn hình chơi game sẽ được hiện ra với một menu có nút resume để bạn chơi tiếp.

TH2:Khi các bạn vào ứng dụng contact, chọn call tới một contact các bạn sẽ thấy một danh sách các ứng dụng hiện ra (Call, Viber, Skype, …) bạn phải chọn một trong các ứng dụng để call. Tương tự với các trường hơp bạn mở môt SMS hay Mail mới. Tại sạo như vậy?

TH3: Bạn đang lượn lờ Facebook bằng điên thoại và ra khỏi khu vực có wifi. Ứng dụng Facebook sẽ thông báo “No Internet Connection’ để thông báo với bạn rằng mobile của bạn không còn kết nối với wifi nữa. Bạn sẽ ko view thêm được bất kì comment hoặc message nào nữa. Tại sao ứng dụng Facebook lại biết được khi nào mobile của bạn không còn kết nối với wifi?

TH4:Bạn đang xem một quảng cáo sản phẩm trong một ứng dụng. Bạn thích sản phẩm này và muốn liên hệ với cửa hàng đặt mua. Bạn click vào số điện thoại của cửa hàng bên dưới sản phẩm. Và thế là mobile của bạn calling đến số điện thoại đó. Và ‘a lô, xin chào bạn đến với cửa hàng….’. Tại sao ứng dụng quảng cáo sản phẩm lại có thể gọi ra ứng dụng call như vậy.

Để giải quyết tất cả các trường hợp cạnh tranh này Android đã đưa ra một khái niệm Broadcast Receiver. Vậy Broadcast Receiver là gi?

Broadcast receiver là một thành phần android cơ bản nó cho phép ứng dụng của bạn đăng kí lắng nghe các sự kiện của hệ thống hoặc của các ứng dụng khác.

Nôm na như sau, trong Android có một thành phần giống như đài truyền hình (broadcaster), nó có nhiệm vụ phát sóng (send) các sự kiện (events) tới các ứng dụng (broadcast receiver or receiver).

Các sự kiện (events) ở đây có thể là các sự kiện của hệ thống như incoming call, new SMS, low battery, wifi ON/OFF, Screen lock/unlock, time zone change… hoặc sự kiện do các ứng dụng thông thường tạo ra. Như vậy ngay ứng dụng của các bạn cũng có thể phát ra các sự kiện và broadcaster sẽ gửi nó đến các ứng dụng có trên thiết bị.

Để ứng dụng của bạn nhận được các sự kiện từ broadcaster như trên thì bạn phải biến ứng dụng của bạn thành một receiver. Có 2 cách để đăng kí một ứng dụng trở thành receiver.

Cách 1: đăng kí trong file AndroidMenifest.xml của ứng dụng

1
2
3
4
5
<receiver android:name=".MyWifiReceiver">
    <intent-filter>
        <action android:name="android.net.wifi.WIFI_STATE_CHANGED" />
    </intent-filter>
</receiver>

Intent-filter định nghĩa ra một loại message thông bao sự kiện trạng thái wifi thay đổi (WIFI_STATE_CHANGED) và đoạn code xml trên đã đăng kí ứng dụng nhận các thông báo liên quan đến sự thay đổi trạng thái của wifi.

“MyWifiReceiver” là một class extends BroadcastReceiver class và bạn phải overwrite hàm onReceive(). Class MyWifiReceiver sẽ nhận và sử lí intent-filter trên trong hàm onReceive()

Cách 2: đăng kí trong code, sử dụng hàm Context.registerReceiver()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
context.registerReceiver(new BroadcastReceiver() {
 
    @Override
    public void onReceive(Context context, Intent intent) {
 
        try {
 
            int wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN);
 
            switch (wifiState) {
                case WifiManager.WIFI_STATE_DISABLING:
                    // DO SOMETHING HERE
                    break;
 
                case WifiManager.WIFI_STATE_DISABLED:
                    // DO SOMETHING HERE
                    break;
 
                case WifiManager.WIFI_STATE_ENABLING:
                    // DO SOMETHING HERE
                    break;
 
                case WifiManager.WIFI_STATE_ENABLED:
                    // DO SOMETHING HERE
                    break;
 
                case WifiManager.WIFI_STATE_UNKNOWN:
                    // DO SOMETHING HERE
                    break;
 
                default:
                    break;
 
            }
        }catch (Exception e) {
 
        }
    }
 
},new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION));

Bạn cũng có thể gỡ bỏ đăng kí broadcast receiver bằng hàm Context.unregisterReceiver()

Có rất nhiều các sự kiện mà ứng dụng có thể đăng kí lắng nghe như đã nói ở trên và mỗi loại sự kiện sẽ tương ứng với các action khác nhau. Bạn có thể tham khảo trong link sau.

https://github.com/ViliusKraujutis/AndroidBroadcastsMonitor/wiki/List-of-all-Broadcast-Intent-actions.-API-17

Bây giờ tôi quay lại giải 4 trường hợp đã nêu ra ở trên để cho các bạn có hình dung thực tế hơn.

TH1: Ứng dụng nghe nhạc sẽ phải đăng kí một receiver để lắng nghe sự kiện incoming call và end call.

1
2
3
4
5
<receiver android:name=".PlayerReceiver">
    <intent-filter>
        <action android:name="android.intent.action.PHONE_STATE" />
    </intent-filter>
</receiver>

Xử lí khi bắt được sự kiện incoming call va end call trong class PlayerReceiver

1
2
3
4
5
6
7
8
9
10
11
public void onReceive(Context context, Intent intent) {
    String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
 
    if(state.equals(TelephonyManager.EXTRA_STATE_RINGING)){
        //Phone is ringing à Stop service play media
    }else if(state.equals(TelephonyManager.EXTRA_STATE_OFFHOOK){
        //Call received
    }else if (state.equals(TelephonyManager.EXTRA_STATE_IDLE)){
        //Call Dropped or rejected à Restart service play media
    }
}

TH2:Khi các bạn vào ứng dụng contact, chọn call tới một contact các bạn sẽ thấy một danh sách các ứng dụng hiện ra (Call, Viber, Skype, …) bởi vì tất cả các ứng dụng trên đều đăng kí nhận sự kiện outgoing call của hệ thống. Vì vậy khi bạn chọn call tới một số nào đó trong contact thì cả 3 ứng dụng này đều có nhận được message outgoing call và tất cả đều có thể start để thực thi cuộc gọi. Lúc này hệ thống sẽ đưa ra danh sách các ứng dụng trên cho người dùng lựa chọn. Nếu úng dụng của bạn đăng kí nhận sự kiện ACTION_NEW_OUTGOING_CALL thì nó cũng được hiện thị trong danh sách ứng dụng trên cùng với Call, Viber, Skype,…

TH3: Ứng dụng Facebook đã đăng kí nhận sự kiện WIFI_STATE_CHANGED vì vậy khi Wifi bị tắt hoặc device đi ra khỏi khu vực có wifi thì ứng dụng sẽ nhận được sự kiện này và hiển thị thông báo “No Internet Connection”.

TH4: Bạn click vào số điện thoại của cửa hàng bên dưới sản phẩm. Và thế là mobile của bạn calling đến số điện thoại đó. Lúc này ứng dụng của bạn sẽ gửi đến hệ thống (broadcaster) một message với action là ACTION_CALL. Hệ thống (broadcaster) sẽ gửi message này tới các ứng dụng. Ứng dụng nào đăng kí xử lí message này thì sẽ được mớ. Trong trường hợp này ứng dụng call sẽ được start. Và người dùng có thể gọi đến số điện thoại ghi trên quảng cáo. Thật là đơn giản phải không. Tương tự như vậy bạn có thể gọi ra các ứng dụng khác như SMS, Browser, …có sẵn trên thiết bị.

Bạn cũng có thể tự định nghĩa ra các sự kiện của riêng mình và gửi chúng đến các ứng dụng khác có trên thiết bị hay chính các ứng dụng khác của bạn. Tôi có thể gợi ý cho các bạn một ví dụ sau.

Bạn có 2 ứng dụng đọc sách và từ điển. Trong ứng dụng đọc sách có thể gửi message do bạn đinh nghĩa ra tới hệ thống (Broadcaster). Hệ thống sẽ gửi message này tới tất cả các ứng dụng nhưng chi có duy nhất ứng dụng từ điển của bạn đăng kí nhận xử lí message này. Và thế là ứng dụng từ điển của bạn được mở khi bạn đang đọc sách.

Tôi xin dừng lại ở đây và hi vọng với các giới thiệu tổng quan ở trên các bạn đã phần nào hình dung ra cơ chế hoạt động của broadcast receiver và một số tình huống cơ bản hay gặp khi làm việc với nó. Các bạn có thể tham khảo kĩ thuật chi tiết hơn trong các link bên dưới đây.

http://www.vogella.com/articles/AndroidBroadcastReceiver/article.html#broadcastreceiver_definition

http://developer.android.com/reference/android/content/BroadcastReceiver.html

http://tapchilaptrinh.vn/2013/05/11/broadcastreceiver/

 

Sức mạnh của thái độ và thói quen

 

“Niềm tin sẽ làm nên suy nghĩ của bạn, Suy nghĩ đó sẽ làm nên lời nói của bạn, Lời nói đó sẽ làm nên hành động của bạn, Hành động đó sẽ làm nên thói quen của bạn, Thói quen đó sẽ làm nên giá trị của bạn, Giá trị đó sẽ làm nên số phận của bạn.”

          — MAHATMA GANDHI —

Hãy bắt đầu từ thái độ

Chúng ta có thể gặp những người ngoài 60 tuổi vẫn rất chịu khó học hỏi, những cụ 70 vẫn lọ mọ học máy tính để chat với con cháu ở xa. Họ là ví dụ của những người có mô thức phát triển (growth mindset, chữ của nhà tâm lí học Carol Dweck ở đại học Stanford). Những người này không để tuổi tác đè nặng, họ học hỏi không ngừng, thích chinh phục những thử thách, thấy khó tìm cách vượt qua, mỗi cơ hội làm việc được tận dụng để hoàn thiện chính mình, luôn coi các chỉ trích hướng vào mình như là cơ hội học tập, và vui mừng khi thấy sự thành công ở người khác. Đây là mẫu tư duy thường gặp ở những người thành công trong các lĩnh vực học thuật, kinh doanh, hay nghệ thuật.

Ngược lại, lại có những người không thích các thử thách, thấy khó khăn là chùn bước, thấy phê phán là phản ứng tức thì, thấy việc làm là ngại, thấy người khác thành công thì ghen tị. Họ được xếp vào những người có mô thức đông cứng (fixed mindset). Sự khác biệt giữa hai mô thức nằm ở thái độ đối với các mục tiêu trong cuộc sống, trong cách thức phản hồi với thử thách và thất bại, niềm tin về nỗ lực và chiến lược, cũng như thái độ với sự thành công của người khác.

Các nghiên cứu về não bộ gần đây cho thấy tính “mềm dẻo” của não người là rất lớn, và nó có thể thay đổi từ tấm bé cho tới lúc già. Số lượng tế bào thần kinh có thể không tăng, nhưng cơ cấu tổ chức não bộ với những kết nối phức tạp giữa các tế bào đó thì thay đổi luôn luôn. Ngay cả những công nghệ như Internet, Facebook, Google cũng khiến cho hoạt động của não bộ thay đổi đến ngạc nhiên; điều này được Nicolas Carr bàn kĩ trong cuốn “Trí tuệ giả tạo” bán chạy. Từ nghiên cứu về mô thức (Mindset) của Carol Dweck,chúng ta có thể rút ra được kết luận hết sức quan trọng là con người hoàn toàn có thể phát triển trong suốt cuộc đời. Cách phân biệt mô thức đông cứng và mô thức phát triển đặt nền móng khoa học vững chắc cho niềm tin về việc học tập suốt đời và sự làm chủ cuộc đời cho những người làm giáo dục-đào tạo trên toàn thế giới. Nó cũng giúp ta chọn lựa một thái độ tích cực và chủ động đối với các nỗ lực vươn tới thành công và hạnh phúc.

Mindset

Thói quen hoạt động như thế nào

Có thái độ thôi chưa đủ, chúng ta phải hành động, và còn phải tạo lập các thói quen tốt. Một nghiên cứu ở Đại học Duke cho thấy 40% hoạt động của chúng ta hằng ngày thuần túy là do thói quen chứ không phải do suy nghĩ thấu đáo. Con số đó cho thấy tầm ảnh hưởng rất lớn của những việc lặp đi lặp lại theo kiểu “phản xạ có điều kiện” này.

Khi đã có thói quen, chúng ta lặp lại các phản xạ trước các tính huống của cuộc sống mà không phải suy nghĩ nhiều. Điều đó có nghĩa là nếu thói quen tốt và được rèn luyện tốt, thì trong phần lớn các tình huống lặp đi lặp lại ấy, chúng ta đang sử dụng cách làm tốt, mang lại kết quả tốt, mà không phí sức. Ngược lại, nếu thói quen xấu, nó sẽ vô tình chung ảnh hưởng tới kết quả mà chúng ta cũng không để ý, cho tới khi sự việc đã muộn rồi.

Tác giả Charles Duhlgg của cuốn sách bán chạy “Sức mạnh của thói quen” dẫn các nghiên cứu khoa học cho biết, sở dĩ chúng ta hình thành các thói quen vì não chúng ta muốn được giảm tải. Ví dụ như khi chúng ta mới học lái xe, đầu óc sẽ suy tính rất mệt mỏi, đạp ga thế nào, phanh thế nào, tay giữ vô lăng chặt như thế có được không, v.v. Nhưng qua thời gian luyện tập, chúng ta hầu như không còn phải nghĩ nhiều về những việc này nữa, chúng
ta hoạt động theo thói quen, và não thì rảnh để quan sát đường, và suy nghĩ về những việc khác khi lái xe.

Thói quen được hình thành theo một chu trình ba bước: Bước Kích hoạt có tác dụng khởi động não bộ vào trạng thái tự động và lựa chọn thói quen để sử dụng; bước Hoạt động (có thể là hoạt động thể chất, tinh thần, cảm xúc); và cuối cùng là Phần thưởng cho những hành động vừa trải qua (phần thưởng này có thể là sự khoan khoái, hoặc được ngợi khen, hoặc bằng những vật có giá trị). Não bộ sẽ đánh giá phần thưởng để xem xét khả năng lưu giữ lại hoạt động đó để lặp lại khi có kích hoạt tương tự. Sự liên hệ Kích hoạt với Phần thưởng sẽ giúp não bộ phát triển cảm giác kì vọng, từ đó dẫn đến hình thành thói quen. Khi được lặp đi lặp lại, chu trình “Kích hoạt, Hoạt động, Phần thưởng” sẽ được tự động hóa, và chúng ta có thói quen.

Bạn không thể bỏ thói quen xấu, nhưng có thể tạo thói quen mới. Nguyên tắc vàng để thay đổi thói quen là giữ nguyên phần Kích hoạt và Phần thưởng, chỉ thay đổi phần Hành động. Lặp đi lặp lại, ta sẽ có thói quen mới. Đây là cách thay đổi thói quen phổ biến và rất hữu hiệu.

Cần 66 ngày để có một thói quen mới

Một nghiên cứu gần đây đăng trên Tạp chí Tâm lí học xã hội Châu Âu cho thấy, trung bình bạn cần 66 ngày để hình thành một thói quen. Trong 66 ngày đó bạn sẽ phải làm đi làm lại điều bạn muốn nó trở thành cơ hữu với con người bạn, sẽ thành “phản xạ” ngay cả khi bạn không còn nghĩ gì về nó nữa. Dĩ nhiên 66 là con số trung bình, con số cụ thể sẽ phụ thuộc vào nhiều yếu tố khác như tính chất của thói quen bạn định hình thành, cá tính của bạn, và cả điều kiện mà bạn đang có. Nhưng con số này cho thấy, để tạo lập thói quen, bạn cần một sự kiên trì đáng kể. Đó là lí do vì sao các chuyên gia về thói quen khuyên chúng ta cần tạo lập sự liên kết chặt chẽ giữa phần Kích hoạt với Phần thưởng để liên tục duy trì động lực khi thực hành thay đổi thói quen.

Hình thành thói quen làm việc hiệu quả dựa trên Scrum

Một trong điểm mạnh cơ bản nhất của Scrum, một phương pháp Agile phổ biến nhất, chính là giúp bạn có một thói quen làm việc tốt: tính toán kĩ khi bắt đầu công việc, lập kế hoạch khả thi và linh hoạt, kiểm soát công việc liên tục, đánh giá cẩn thận kết quả đạt được và cải tiến liên tục. Các bước đó được sắp đặt và kết hợp logic với nhau, lặp đi lặp lại để sớm hình thành nếp nghĩ, nếp làm việc một cách khoa học và hiệu quả. Scrum không chỉ
giúp bạn sớm nhìn ra hiệu quả công việc, vui sướng ngay trong khi làm việc, mà còn duy trì một thói quen tốt.

Tác giả của Scrum, tiến sĩ Jeff Sutherland đã từng viết trên blog cá nhân và trong cuốn sách “Scrum: Nghệ thuật làm được gấp đôi chỉ trong một nửa thời gian” về những câu chuyện thành công trong sử dụng tư duy Scrum vào mọi mặt của đời sống, từ việc dạy học ở trường phổ thông, đến việc marketing, hay tổ chức công việc hằng ngày ở nhà thờ. Chúng ta gọi thói quen này là Scrumlife, để chỉ việc áp dụng tư duy Scrum vào hình thành thói quen
làm việc hằng ngày cho thật tốt.

Hãy bắt đầu Scrumlife đơn giản như thế này:

  1. Chúng ta sẽ khởi đầu tuần làm việc bằng việc lập kế hoạch, gồm 2 bước: xác định việc cần làm, và cách để hiện thực hóa việc cần làm.
  2. Xong rồi ta cập nhật các công việc đó lên Bảng công việc (kanban board), bắt đầu làm những việc có độưu tiên cao hơn, dần dần cho tới hết. Mỗi ngày ta thực hiện 15 phút DailyScrum để tự theo dõi tiến độvà thích ứng với các tình huống thay đổi.
  3. Cuối tuần ta rà soát lại xem đã làm việc gì, so với dự kiến trong kế hoạch đầu tuần thì hoàn thành được bao nhiêu phần trăm, so với tuần trước thì thế nào?
  4. Cuối cùng, ta suy nghĩ về cách làm việc , có ổn không, cần cải tiến gì không. Hãy cố gắng rút ra ít nhất một điều cải tiến, để tuần sau làm tốt hơn. Lưu ý đây là cải tiến cách làm; ví dụ, nếu tuần rồi ta viết thư điện tử cho sếp mà quên không đính kèm file, dẫn đến sếp bực mình, và đây là lỗi lặp lại lần thứ 3 rồi, thì ta có thể kĩ đến cách thức viết thư mới (ví dụ: đảo ngược quy trình viết thư: Đính kèm đầu tiên, rồi mới viết tiêu đề, rồi viết nội dung, cuối cùng là đọc lại và thêm phần To và gửi cho sếp).

Cách làm này áp dụng cho 1 tuần làm việc hăng say và duy trì sự hiệu quả liên tục. Nhưng bạn cũng có thể áp dụng tư duy ấy cho mỗi ngày làm việc với cấu trúc tương tự.

Trước khi bắt đầu thực hiện thói quen này, hãy nghĩ về mối liên hệ Kích hoạt – Phần thưởng. Phần thưởng cho việc hình thành thói quen này là gì? Bạn hãy nghĩ về những mối bận tâm thường trực khi làm việc: Bạn bắt đầu và kết thúc công việc trong tuần thế nào? Năng suất ra sao? Có hiệu quả không? Có thành tựu không? Có thấy vui sướng khi làm việc không? Và lưu ý, hãy tự thưởng cho mình, hoặc tìm kiếm các phần thưởng từ bên ngoài (từ Sếp, từ gia đình, bạn bè …) khi bạn hoàn thành tốt công việc. Nó không chỉ là cơ chế duy trì động lực tạo thói quen, mà còn là sự tận hưởng cuộc sống.

Nếu bạn thực hành Scrum được 2 ngày và thấy hơi gò bó, thì xin chúc mừng bạn, bạn đang trong quá trình hình thành một thói quen mới. Nhưng hãy nhớ, bạn còn khoảng 64 ngày nữa. Cứ lặp đi lặp lại sẽ hình thành một nhịp làm việc và sinh hoạt đều đặn, dần trở nên tự nhiên và phát huy tác dụng. Hãy thử vài tuần đi, bạn sẽ thấy rất nhiều điều bất ngờ đấy.

scrum-life

Bạn có biết: Scrum từng được đề nghị trao giải Nobel về quản trị?

Đó chính là giải (không có thật) được trao bởi một cây viết quen thuộc trên tạp chí nổi tiếng Forbes, Steve Denning, cho hai ông Ken Schwaber và Jeff Sutherland vì đã có công phát minh ra phương pháp tổ chức công việc nổi tiếng Scrum làm thay đổi thế giới phần mềm trong hơn một thập kỉ qua.

Chuyện giải Nobel là do ông Denning “bịa” cho vui, nhưng để nhấn mạnh sự hiệu quả nổi bật mà Scrum có thể mang lại cho các nhóm làm việc trên khắp thế giới.

Trong một bài viết năm 2011, Denning tóm tắt 10 đặc điểm của Scrum (có thể không giống cách nói của chính các tác giả của Scrum):

  1. Tổ chức công việc theo các chu trình ngắn (gọi là phân đoạn)
  2. Khi nhóm làm việc của họ trong các chu trình ngắn này, cấp quản lí không can thiệp (tức nhóm được trao quyền tối đa)
  3. Nhóm báo cáo trực tiếp cho khách hàng, không phải cho nhà quản lí 4. Nhóm ước tính thời gian để hoàn thành công việc
  4. Nhóm quyết định khối lượng công việc để làm trong phân đoạn 6. Nhóm quyết định cách hoàn thành công việc trong phân đoạn

LINK: http://tapchilaptrinh.vn/2016/04/17/suc-manh-cua-thai-do-va-thoi-quen/

 

CRUD USING .NET CORE 1.0, ANGULARJS2, WEBAPI

In our previous article we have seen how to startup with .Net Core. In this article we will take a look on database operation according to previous sample application based previous concept.

If you are new to .Net Core, Please read previous post about .Net Core Startup

In this article we are going to explore,

  1. Create Database
  2. Use Entity Framework Core (Db First Approach),
    1. Overview EF Core
    2. Install Entity Framework
    3. Create Models
    4. Configure EF Service
  3. Use MVC 6
    1. Overview MVC6
    2. Use WebAPI
  4. Use AngularJS2
    1. Component,
    2. Route
    3. Service
  5. Configure Server
    1. Run App inside/outside IIS

Let’s get started with step by step:

Create Database Before we get started with IDE lets create a new database using SSMS2014. Name it as PhoneBook.

Create a table Named Contacts, copy & run the below script in SSMS2014

USE [PhoneBook]
GO

/****** Object:  Table [dbo].[Contacts]    Script Date: 8/7/2016 11:28:55 AM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Contacts](
	[ContactID] [int] IDENTITY(1,1) NOT NULL,
	[FirstName] [nvarchar](50) NULL,
	[LastName] [nvarchar](50) NULL,
	[Phone] [nvarchar](50) NULL,
	[Email] [nvarchar](50) NULL,
 CONSTRAINT [PK_Contacts] PRIMARY KEY CLUSTERED 
(
	[ContactID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO
 

Let’s get started, following our previous topic I am going to use previous sample application, open it with Visual Studio 2015.

core_crud_1

It will automatically started restoring the dependencies. Build & run it. The application is working perfectly.

Install Entity Framework:Before install let’s have an overview on EF Core new Features:

  • Modelling: This includes Basic modelling, Data annotations, Relationships and much more.
  • Change Tracking:This includes Accessing tracked state, Snapshot, Notification change tracking.
  • SaveChanges: This includes Basic save functionality,Async SaveChanges,Transactions.
  • Query: This includes Basic LINQ support, Async query, Raw SQL queries
  • Database schema management: This includes Database creation/deletion APIs, Relational database migrations, and Reverse engineer from database.
  • Database providers: This includes EntityFramework.SqlServer, Sqlite, InMemory
  • Platforms: Supports Universal Windows Platform (UWP), .NET Core, Full .NET

Get more details about EF Core. Let’s add folders for Entity models in our sample app solution. core_crud_3

DbEntities: for model entities. The installation of EF is pretty much simple. Open project.json file, point tools section modify the section with below line.

"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
"Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0"

core_crud_4

Save changes after modification.

core_crud_5

Packages will automatically restored. Let’s get explanation what are those.

EntityFrameworkCore.SqlServer: Database Provider, that allows Entity Framework Core to be used with Microsoft SQL Server.

EntityFrameworkCore.Tools: Command line tool for EF Core. Includes Commands

For Package Manager Console:

  • Scaffold-DbContext,
  • Add-Migration,
  • Udate-Database

For Command Window:

  • dotnet ef dbcontext scaffold

We will see how to use both command. EntityFrameworkCore.SqlServer.Design: Design-time, that allows Entity Framework Core functionality (EF Core Migration) to be used with Microsoft SQL Server. To access the Command line tools we need to add EntityFrameworkCore.Tools intools section of our project.json.

"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"

core_crud_6

Save changes after modification. Command in Package Manager Console:Open package manager console.

core_crud_7

Input below command then hit enter,

Scaffold-DbContext "Server=DESKTOP-4T79RA1;Database=PhoneBook;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models/DbEntities

 core_crud_8core_crud_9

Command in Command Window:Open Command Window navigate to project directory, type

D:\Article\ASP-CORE\CRUD\CoreMVCAngular2\src\CoreMVCAngular>dotnet ef –help

Here a list of option will be shown in command window, we are going to use dbcontext in Commands.

core_crud_10

NextInput below command then hit enter,

D:\Article\ASP-CORE\CRUD\CoreMVCAngular2\src\CoreMVCAngular>dotnet ef dbcontext scaffold "Server=DESKTOP-4T79RA1;Database=PhoneBook;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer --output-dir Models/CwEntities

core_crud_11

Here is a screen shot of both process that execute & generate models. We will keep DbEntities folder to work with & will delete the other folder.

core_crud_12

Configure EF Service: In PhoneBookContext Class add constructor

public PhoneBookContext(DbContextOptions<PhoneBookContext> options) :
       base(options)
{
}
 

In Startup class we need to enable EF services providing the connectionstring to

public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
  var connection = @"Server=DESKTOP-4T79RA1;Database=PhoneBook;Trusted_Connection=True;";
  services.AddDbContext<PhoneBookContext>(options => options.UseSqlServer(connection));
}
 

We have configure the EF services in our application, next we will work with MVC6 that is included in ASP.NET Core.

MVC 6:We have already discuss about MVC6 in our previous post, let’s have an overview on MVC6 new Features, once again:

  1. MVC+Web API+Web Pages = MVC6
  2. No System.Web
  3. Web pages & HTTP services is Unified
  4. Dependency injection built in
  5. Dynamic code compilation (Roslyn compiler)
  6. Open source &
  7. Support cross-platform build & run.
  8. Can be hosted in IIS or self-hosted(Outside IIS)

Ok, now let’s add a WebApi Controller to perform CRUD operation to database table. core_crud_13 In Solution Explorer add a new api folder, right click on it > Add New Item > Web API Controller Class > Add. Modify the initial template.

API Controller

[Route("api/[controller]")]
public class ContactController : Controller
{
    private PhoneBookContext _ctx = null;
    public ContactController(PhoneBookContext context)
    {
        _ctx = context;
    }
}
 

core_crud_14

You may notice that there is a new pattern [ ] in MVC6 attribute route, which is [RouteToken]. This mean that the route token is automatically take the controller name.

Like [Route("api/[controller]")] > [Route("api/Contact")]

Another thing is, we know Web API produces XML by default, now in MVC 6 we can set an attribute to change default produces to JSON type by putting attribute in Class label or on method label. In our case we have set it on method label.

[HttpGet("GetContact"), Produces("application/json")]

GET

// GET: api/Contact/GetContact
[HttpGet("GetContact"), Produces("application/json")]
public async Task<object> GetContact()
{
    List<Contacts> contacts = null;
    object result = null;
    try
    {
        using (_ctx)
        {
            contacts = await _ctx.Contacts.ToListAsync();
            result = new
            {
                contacts
            };
        }
    }
    catch (Exception ex)
    {
        ex.ToString();
    }
    return result;
}
 

POST

// POST api/Contact/PostContact
[HttpPost, Route("PostContact")]
public async Task<object> PostContact([FromBody]Contacts model)
{
    object result = null; int message = 0;
    if (model == null)
    {
        return BadRequest();
    }
    using (_ctx)
    {
        using (var _ctxTransaction = _ctx.Database.BeginTransaction())
        {
            try
            {
                _ctx.Contacts.Add(model);
                await _ctx.SaveChangesAsync();
                _ctxTransaction.Commit();
                message = (int)responseMessage.Success;
            }
            catch (Exception e)
            {
                _ctxTransaction.Rollback();
                e.ToString();
                message = (int)responseMessage.Error;
            }

            result = new
            {
                message
            };
        }
    }
    return result;
}
 

PUT

// PUT api/Contact/PutContact/5
[HttpPut, Route("PutContact/{id}")]
public async Task<object> PutContact(int id, [FromBody]Contacts model)
{
    object result = null; int message = 0;
    if (model == null)
    {
        return BadRequest();
    }
    using (_ctx)
    {
        using (var _ctxTransaction = _ctx.Database.BeginTransaction())
        {
            try
            {
                var entityUpdate = _ctx.Contacts.FirstOrDefault(x => x.ContactId == id);
                if (entityUpdate != null)
                {
                    entityUpdate.FirstName = model.FirstName;
                    entityUpdate.LastName = model.LastName;
                    entityUpdate.Phone = model.Phone;
                    entityUpdate.Email = model.Email;

                    await _ctx.SaveChangesAsync();
                }
                _ctxTransaction.Commit();
                message = (int)responseMessage.Success;
            }
            catch (Exception e)
            {
                _ctxTransaction.Rollback(); e.ToString();
                message = (int)responseMessage.Error;
            }

            result = new
            {
                message
            };
        }
    }
    return result;
}
 

DELETE

// DELETE api/Contact/DeleteContactByID/5
[HttpDelete, Route("DeleteContactByID/{id}")]
public async Task<object> DeleteContactByID(int id)
{
    object result = null; int message = 0;
    using (_ctx)
    {
        using (var _ctxTransaction = _ctx.Database.BeginTransaction())
        {
            try
            {
                var idToRemove = _ctx.Contacts.SingleOrDefault(x => x.ContactId == id);
                if (idToRemove != null)
                {
                    _ctx.Contacts.Remove(idToRemove);
                    await _ctx.SaveChangesAsync();
                }
                _ctxTransaction.Commit();
                message = (int)responseMessage.Success;
            }
            catch (Exception e)
            {
                _ctxTransaction.Rollback(); e.ToString();
                message = (int)responseMessage.Error;
            }

            result = new
            {
                message
            };
        }
    }
    return result;
}
 

So our Web API is ready to dealing with data to database, it’s time to work with client side scripting.  

AngularJS2: Our WebAPI is ready to deal with data from server. Now we are going to work in client-side code with typescript (.ts) files. First of all we need to create a master page to present our views in it. core_crud_15

Then we need to point this html file while app start, so let’s go to the startup.cs file to add below code snippet. This is the configuration for the default files Middleware.

Startup.cs

// app-specific root page(Index.html)
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("/Index.html");

need to add library

using Microsoft.AspNetCore.Builder;

Now add script library reference to the html page & define view point to load our app component views.

<spa-app>
   <p>
      <img src="img/ajax_small.gif" />  Please wait ...
   </p>
</spa-app>

Then we need to reference our bootstrap file in our page, that import & enable the our angular script to the page.

<script>
   System.config({ packages: { 'app': { defaultExtension: 'js' } }, });
   System.import('app/main').then(null, console.error.bind(console));
</script

Let’s put together all those in Index.html file.

Index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title></title>

    <base href="/">
    <script>document.write('<base href="' + document.location + '" />');</script>
    <script src="../lib-npm/es6-shim/es6-shim.js"></script>
    <script src="../lib-npm/angular2/angular2-polyfills.js"></script>
    <script src="../lib-npm/systemjs/system.src.js"></script>
    <script src="../lib-npm/rxjs/Rx.js"></script>
    <script src="../lib-npm/angular2/angular2.js"></script>
    <script src="../lib-npm/angular2/router.js"></script>
    <script src="../lib-npm/angular2/http.js"></script>

    <link href="../lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div class="container">
        <spa-app>
            <p>
                <img src="img/ajax_small.gif" />  Please wait ...
            </p>
        </spa-app>

    </div>
    <script src="../lib/jquery/dist/jquery.min.js"></script>
    <script src="../lib/bootstrap/dist/js/bootstrap.min.js"></script>
    <script>
        System.config({ packages: { 'app': { defaultExtension: 'js' } }, });
        System.import('app/main').then(null, console.error.bind(console));
    </script>
</body>
</html>
 

  Bootstrap, Model, Component & Route Main.ts

/*This is the spa bootstrap File*/

//---------Import Angular2------------
import {bootstrap}    from 'angular2/platform/browser';
import {enableProdMode, provide} from 'angular2/core';

//---------Import External Components(Main Component)---------
import {MainComponent} from './app.component';

//---------Bootstrap Component---------
enableProdMode();
bootstrap(MainComponent);
App.component.ts

Hide   Shrink    Copy Code
/*Component Default view For SpaRoute */

//---------Import Angular2------------
import {Component, provide} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES, 
        ROUTER_PROVIDERS, LocationStrategy, 
        HashLocationStrategy, APP_BASE_HREF}                  from 'angular2/router';
 
//---------Import External Components---------
import {Home} from './home/home.component';
import {Contact} from './contact/contact.component';

//---------Declare Components---------
@Component({
    selector: 'spa-app',
    directives: [ROUTER_DIRECTIVES], //decorate link 
    templateUrl: 'app/main.view.html',
    providers: [
        ROUTER_PROVIDERS,
        //provide(APP_BASE_HREF, { useValue: '/' })
        provide(LocationStrategy, { useClass: HashLocationStrategy })
    ]
})

//---------Declare Route Config---------
@RouteConfig([
    { path: '/', name: 'Home', component: Home, useAsDefault: true },
    { path: '/Contact/...', name: 'Contact', component: Contact }
])


//---------Export This Component Class---------
export class MainComponent {
    title: string;
    constructor() {
        this.title = 'Welcome to [.NetCore+MVC6+Angular2] SPA';
    }
}


  Home.ts

import {Component} from 'angular2/core';

@Component({
    selector: 'home',
    templateUrl: `app/home/home.view.html`
})
export class Home {
    constructor() {   
    }
}
Contact.model.ts

Hide   Copy Code
export class ContactModel {
    contactId: number;
    firstName: string;
    lastName: string;
    phone: string;
    email: string;
}


Contact.component.ts

//---------Import Angular2------------
import {Component}                                            from 'angular2/core';
import {ROUTER_DIRECTIVES, RouteConfig}                       from 'angular2/router';

//---------Import External Components---------
import {ContactMain}                                          from './contact.main';

//---------Declare Components---------
@Component({
    selector: 'contacts',
    template: `<router-outlet></router-outlet>`,
    directives: [ROUTER_DIRECTIVES]
})
@RouteConfig([
    { path: '/', name: 'ManageContact', component: ContactMain, useAsDefault: true },
])
export class Contact {
    constructor() { }
}
 

  Contact.main.ts

//---------Import Angular2------------
import {Component, OnInit}                                     from 'angular2/core';
import {HTTP_PROVIDERS, Http}                                  from 'angular2/http';
import {ROUTER_DIRECTIVES, RouteConfig}                        from 'angular2/router';
import {FORM_DIRECTIVES,
    FormBuilder, Control, ControlGroup, Validators}            from 'angular2/common';

//---------Import External Components---------
import {ContactModel}                                          from './contact.model';
import {ContactService}                                        from './contact.service';
import {customvalidators}                                      from './customvalidators';

//---------Declare Components---------
@Component({
    selector: 'contact-list',
    templateUrl: `app/contact/contact.view.html`,
    directives: [ROUTER_DIRECTIVES, FORM_DIRECTIVES],
    providers: [ContactService, HTTP_PROVIDERS]
})

//---------Export This Component Class---------
export class ContactMain implements OnInit {

    public resmessage: string;
    public addmessage: string;
    public listmessage: string;
    public contact: ContactModel;
    public contacts: ContactModel[];
    public editContactId: any

    //Form Control
    contactForm: ControlGroup;
    firstName: Control;
    email: Control;
    phone: Control;

    //Constructor
    constructor(private builder: FormBuilder,
        private contactService: ContactService) {
        this.addmessage = 'Add New Contact';
        this.listmessage = 'All Contact';
        this._formGroup();
    }

    ngOnInit() {
        this.resmessage = "";
        this.editContactId = 0;
        this.getContacts();
    }

    //Form Group
    _formGroup() {
        this.firstName = new Control('', Validators.required);
        this.email = new Control('', Validators.compose([Validators.required, customvalidators.emailValidator]));
        this.phone = new Control('');

        this.contactForm = this.builder.group({
            firstName: this.firstName,
            email: this.email,
            phone: this.phone
        });
    }

    //Get All 
    getContacts() {
        //debugger
        this.contactService.getContacts().subscribe(
            contacts => this.contacts = contacts
        );
    }

    //Save Form
    saveContact(contact) {
        //debugger
        this.contactService.saveContact(contact)
            .subscribe(response => {
                this.resmessage = response;
                this.getContacts();
                this.reset();
            });
    }

    //Get by ID
    editContact(e, m) {
        //debugger
        e.preventDefault();
        this.editContactId = m.contactId;
        this.contactService.getContactByID(m.contactId)
            .subscribe(response => {
                this.contact = response;
                this.firstName.updateValue(this.contact.firstName);
                this.email.updateValue(this.contact.email);
                this.phone.updateValue(this.contact.phone);
            });
    }

    //Save Form
    updateContact(contact: any) {
        //debugger
        if (this.editContactId > 0) {
            this.contactService.updateContact(contact, this.editContactId)
                .subscribe(response => {
                    this.resmessage = response;
                    this.getContacts();
                    this.reset();
                });
        }
    }

    //Delete
    deleteContact(e, m) {
        //debugger
        e.preventDefault();
        var IsConf = confirm('You are about to delete ' + m.firstName + '. Are you sure?');
        if (IsConf) {
            this.contactService.deleteContact(m.contactId)
                .subscribe(response => {
                    this.resmessage = response;
                    this.getContacts();
                });
        }
    }

    reset() {
        this.editContactId = 0;
        this._formGroup();
    }
}
 

Let’s take a closer look at below code snippet, we have the service method call in hare, but the unknown term Subscribe -What is it for? Below we have a simple explanation.

this.contactService.getContacts().subscribe(
            contacts => this.contacts = contacts
        );

Subscribe:The subscriber function to be passed to the Observable constructor.  

Services In our service file we have Http service [Get, GetByID, Post, Put, Delete] that connect with WebAPI to perform operation Create, Read, Update & Delete. GET ALL:Performs a request with `get` http method.For Collection of Object

/Get
    getContacts(): Observable<ContactModel[]> {
        //debugger
        return this._http.get(this._getUrl)
            .map(res => <ContactModel[]>res.json())
            .catch(this.handleError);
    }

GET By ID:Performs a request with `get` http method.For Single Object

//GetByID
    getContactByID(id: string): Observable<ContactModel> {
        //debugger
        var getByIdUrl = this._getByIdUrl + '/' + id;
        return this._http.get(getByIdUrl)
            .map(res => <ContactModel>res.json())
            .catch(this.handleError);
    }

POST:Performs a request with `post` http method.

//Post
    saveContact(contact: ContactModel): Observable<string> {
        //debugger
        let body = JSON.stringify(contact);
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });

        //http.post(url: string, body: string, options ?: RequestOptionsArgs): Observable<Response>
        return this._http.post(this._saveUrl, body, options)
            .map(res => res.json().message)
            .catch(this.handleError);
    }

PUT:Performs a request with `put` http method.

//Put
    updateContact(contact: ContactModel, id: string): Observable<string> {
        //debugger
        var updateUrl = this._updateUrl + '/' + id;
        var body = JSON.stringify(contact);
        var headers = new Headers();
        headers.append('Content-Type', 'application/json');

        //http.post(url: string, body: string, options ?: RequestOptionsArgs): Observable<Response>
        return this._http.put(updateUrl, body, { headers: headers })
            .map(response => response.json().message)
            .catch(this.handleError);
    }

DELETE:Performs a request with `delete` http method.

//Delete
    deleteContact(id: string): Observable<string> {
        //debugger
        var deleteByIdUrl = this._deleteByIdUrl + '/' + id

        //http.post(url: string, options ?: RequestOptionsArgs): Observable<Response>
        return this._http.delete(deleteByIdUrl)
            .map(response => response.json().message)
            .catch(this.handleError);
    }

Observable : [Observable<T>] A representation of any set of values over any amount of time. This the most basic building block of RxJS. Let’s put it together in Contact.service file.

Contact.service.ts

import {Injectable, Component}                            from 'angular2/core';
import {Http, Request, RequestMethod, Response,
    RequestOptions, Headers}                              from 'angular2/http';
import 'rxjs/Rx';
import {Observable}                                       from 'rxjs/Observable';
import {ContactModel}                                     from './contact.model';

@Component({
    providers: [Http]
})

@Injectable()
export class ContactService {
    public headers: Headers;
    constructor(private _http: Http) {
    }

    public _saveUrl: string = '/api/Contact/PostContact/';
    public _updateUrl: string = '/api/Contact/PutContact/';
    public _getUrl: string = '/api/Contact/GetContact/';
    public _getByIdUrl: string = '/api/Contact/GetContactByID/';
    public _deleteByIdUrl: string = '/api/Contact/DeleteContactByID/';

    //Get
    getContacts(): Observable<ContactModel[]> {
        //debugger
        return this._http.get(this._getUrl)
            .map(res => <ContactModel[]>res.json())
            .catch(this.handleError);
    }

    //GetByID
    getContactByID(id: string): Observable<ContactModel> {
        //debugger
        var getByIdUrl = this._getByIdUrl + '/' + id;
        return this._http.get(getByIdUrl)
            .map(res => <ContactModel>res.json())
            .catch(this.handleError);
    }

    //Post
    saveContact(contact: ContactModel): Observable<string> {
        //debugger
        let body = JSON.stringify(contact);
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });

        //http.post(url: string, body: string, options ?: RequestOptionsArgs): Observable<Response>
        return this._http.post(this._saveUrl, body, options)
            .map(res => res.json().message)
            .catch(this.handleError);
    }

    //Put
    updateContact(contact: ContactModel, id: string): Observable<string> {
        //debugger
        var updateUrl = this._updateUrl + '/' + id;
        var body = JSON.stringify(contact);
        var headers = new Headers();
        headers.append('Content-Type', 'application/json');

        //http.post(url: string, body: string, options ?: RequestOptionsArgs): Observable<Response>
        return this._http.put(updateUrl, body, { headers: headers })
            .map(response => response.json().message)
            .catch(this.handleError);
    }

    //Delete
    deleteContact(id: string): Observable<string> {
        //debugger
        var deleteByIdUrl = this._deleteByIdUrl + '/' + id

        //http.post(url: string, options ?: RequestOptionsArgs): Observable<Response>
        return this._http.delete(deleteByIdUrl)
            .map(response => response.json().message)
            .catch(this.handleError);
    }

    private handleError(error: Response) {
        return Observable.throw(error.json().error || 'Opps!! Server error');
    }
}

Let’s discus about form in angular2, there are two strategy of angular2 form

  1. Template-driven
  2. Model-driven

 Template-driven In template-driven form directive are added declaratively in the template.

<input id="firstName" type="text"
       class="form-control"
       placeholder="FirstName" [ngFormControl]="firstName" required>

Noticed that the validator is added declaratively with the input element “required”.  

Model-driven In our sample app we have used model-driven form that has ngFormModel & ngFormControl. Here ngFormControl is bind with input element to get the input values through the control.

ngFormModel:binding it to a controller variable “contactForm”

<form [ngFormModel]="contactForm">

ngFormControl

<input id="firstName" type="text" 
                       class="form-control" 
                       placeholder="FirstName" [ngFormControl]="firstName">

ControlGroup is contain of several Controls.

//Form Control
contactForm: ControlGroup;
firstName: Control;
email: Control;
phone: Control;

An Injected FormBulder use the builder to create the control group which is pass as key value pairs.

private builder: FormBuilder

//Form Group
_formGroup() {
    //Set Initial Values to the Control & Validators
    this.firstName = new Control('', Validators.required);
    this.email = new Control('', Validators.compose([Validators.required, customvalidators.emailValidator]));
    this.phone = new Control('');

    //Pass the grouped controls as key value pairs
    this.contactForm = this.builder.group({
        firstName: this.firstName,
        email: this.email,
        phone: this.phone
    });
}

The validations are also checked in our component. Below we have our Model-driven complete form. Form

<form [ngFormModel]="contactForm">
            <div class="form-group" [ngClass]="{ 'has-error' : !firstName.valid }">
                <label class="control-label" for="firstName">Username</label>
                <em *ngIf="!firstName.valid">*</em>
                <input id="firstName" type="text" 
                       class="form-control" 
                       placeholder="FirstName" [ngFormControl]="firstName">
            </div>

            <div class="form-group" [ngClass]="{ 'has-error' : !email.valid }">
                <label class="control-label" for="email">Email</label>
                <em *ngIf="!email.valid">*</em>
                <input id="email" type="email" 
                       class="form-control" 
                       placeholder="Email" [ngFormControl]="email">
            </div>

            <div class="form-group">
                <label class="control-label" for="phone">Phone</label>
                <input id="phone" type="text" class="form-control" placeholder="Phone" [ngFormControl]="phone">
            </div>

            <div class="form-group">
                <button type="submit" class="btn btn-danger" (click)="reset()">Reset</button>
                <button type="submit" class="btn btn-primary" (click)="saveContact(contactForm.value)" 
                        *ngIf="editContactId == 0" 
                        [disabled]="!contactForm.valid">Create</button>
                <button type="submit" class="btn btn-success" (click)="updateContact(contactForm.value)" 
                        *ngIf="editContactId > 0" 
                        [disabled]="!contactForm.valid">Update</button>
            </div>
</form>
 

  Here is the complete contact view page which we have used in our application.

Contact.view.html

<div class="row">

    <div class="col-sm-4">
        <h3>Phone Book {{addmessage}}</h3>
        <form [ngFormModel]="contactForm">
            <div class="form-group" [ngClass]="{ 'has-error' : !firstName.valid }">
                <label class="control-label" for="firstName">Username</label>
                <em *ngIf="!firstName.valid">*</em>
                <input id="firstName" type="text" 
                       class="form-control" 
                       placeholder="FirstName" [ngFormControl]="firstName">
            </div>

            <div class="form-group" [ngClass]="{ 'has-error' : !email.valid }">
                <label class="control-label" for="email">Email</label>
                <em *ngIf="!email.valid">*</em>
                <input id="email" type="email" 
                       class="form-control" 
                       placeholder="Email" [ngFormControl]="email">
            </div>

            <div class="form-group">
                <label class="control-label" for="phone">Phone</label>
                <input id="phone" type="text" class="form-control" placeholder="Phone" [ngFormControl]="phone">
            </div>

            <div class="form-group">
                <button type="submit" class="btn btn-danger" (click)="reset()">Reset</button>
                <button type="submit" class="btn btn-primary" (click)="saveContact(contactForm.value)" 
                        *ngIf="editContactId == 0" 
                        [disabled]="!contactForm.valid">Create</button>
                <button type="submit" class="btn btn-success" (click)="updateContact(contactForm.value)" 
                        *ngIf="editContactId > 0" 
                        [disabled]="!contactForm.valid">Update</button>
            </div>
        </form>
        <span class="warning">{{resmessage}}</span>
    </div>
    <div class="col-sm-8">
        <h3>Phone Book {{listmessage}}</h3>
        <table style="width:100%" class="table table-striped">
            <tr>
                <th>ID</th>
                <th>Firstname</th>
                <th>Email</th>
                <th>Phone</th>
                <th>Option</th>
            </tr>
            <tr *ngFor="#contact of contacts">
                <td>{{ contact.contactId }}</td>
                <td>{{ contact.firstName }}</td>
                <td>{{ contact.email }}</td>
                <td>{{ contact.phone }}</td>
                <td>
                    <a href="javascript:void(0)"
                       (click)="deleteContact($event, contact)"
                       class="btn btn-danger btn-xs pull-right">Delete</a>
                    <a href="javascript:void(0)"
                       (click)="editContact($event, contact)"
                       class="btn btn-primary btn-xs pull-right">Edit</a>
                </td>
            </tr>
        </table>
    </div>
</div>
 

  This is all about from our angular section in our sample app, that we have used to perform client-end operation, now it’s time to build & run the application. Next we will get overview on server configuration.  

ConfigureServer: Outside IIS (Weblistener):We can host & run our application without an IIS environment, we need to add command object that tell the hosting to use the HTTP server weblistener (Windows-only).

"commands": {
    "OutsideIIS": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.Weblistener --server.urls http://localhost:5001"
  },

Make sure the dependencies is exist

"Microsoft.AspNetCore.Server.WebListener": "0.1.0"

Now go to solution explorer right click project > properties > Debug , change the profile to OutsideIIS. core_crud_16 Set launch URL, then save & run the application. core_crud_17 Server will start with hosting environment details

core_crud_18

The application is running on url http://localhost:5000 with details request info.

core_crud_19

Inside IIS (Kestrel):Kestrel is a cross-platform web server which is run behind IIS or Nginx.

core_crud_20

Change to IIS Express from navigation bar dropdown list to run the application using IIS. Ok let’s run our application to see the way how it’s work.

Output: Here we can see the application output with welcome message & the data is listed from the server through our MVC6 –WebAPI. Here we can perform CRUD operation with required validation.  

core_crud_21

Hope this will help :)

LINK: https://www.codeproject.com/Articles/1118189/CRUD-USING-NET-CORE-ANGULARJS-WEBAPI

 

Draw.io: vẽ sơ đồ online, nhanh và mạnh, miễn phí, không giới hạn số lượng

Draw.io là một công cụ vẽ sơ đồ rất mạnh mẽ, hỗ trợ nhiều hình khối, chạy online không cần cài đặt mà lại miễn phí và không bị giới hạn số biểu đồ như nhiều tool vẽ nền web khác. Bạn có thể vẽ sơ đồ về mạng, điện, phác thảo vị trí các căn phòng trong nhà, hay vẽ các quy trình kinh doanh, vận hành, sản xuất. Anh em làm công nghệ cũng sẽ thích Draw.io vì nó cho phép bạn vẽ hàng tá sơ đồ thiết kế phần mềm, phần cứng và hệ thống. Draw.io có thư viện template rất phong phú để bạn có thể bắt đầu nhanh hơn, không phải tự mình vẽ lại hết tự đầu.

https://www.draw.io

Sau khi vẽ xong, bạn có thể:

  • Lưu file draw.io vào Google Drive để kì sau lôi ra xài cho tiện. Vì file ở trên Drive nên khi nào mở ra cũng có, an toàn
  • Download file về máy tính. Hỗ trợ các định dạng hình ảnh, PDF và ảnh vector SVG để bạn nhúng vào các ứng dụng, tài liệu khác.

Tips: Anh em có thể đổi sang theme đẹp hơn bằng cách vào Extra > Theme >Atlas

Draw.io hiện là công cụ vẽ hàng đầu mà mình lựa chọn vì không phải cài đặt, không phải phụ thuộc những bộ phận mềm vẽ đắt tiền (Microsoft Visio chẳng hạn). Hơn nữa, bạn có thể dễ dàng chia sẻ file sơ đồ cho người khác mà họ cũng không cần phải có phần mềm chuyên dụng để đọc, chỉ cần mở trình duyệt là đủ, nhiều người thậm chí có thể xem cùng lúc. Mời anh em xài thử.


Draw_io_ve_so_do_free_mien_phi_online_3.jpg

Draw_io_ve_so_do_free_mien_phi_online_1.jpg

Draw_io_ve_so_do_free_mien_phi_online_2.jpg

Using ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate to Create NLayered Web Application (Part I)

A step by step guide to create a web application based on ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate frameworks with automated tests.

Contents

  • Introduction
    • Prerequirements
  • Create the Application
  • Developing the Application
    • Creating a Task Entity
    • Adding Task to DbContext
    • Creating the First Database Migration
    • Creating the Database
    • Task Application Service
    • Testing the TaskAppService
    • Task List View
      • Adding a New Menu Item
      • Creating the TaskController and ViewModel
      • Task List Page
      • Localization
      • Filtering Tasks
    • Automated Testing Task List Page
  • More
  • Article History

Introduction

This is first part of the "Using ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate to Create NLayered Web Application" article series. See other parts:

In this article, I'll show to create a simple layered web application using the following tools:

We will also use Log4Net and AutoMapper which are included in ABP startup template by default. We will use the following techniques:

The project will be developed here is a simple task management application where tasks can be assigned to people. Instead of developing the application layer by layer, I will go to vertical and change the layers as the application grows. While the application grows, I will introduce some features of ABP and other frameworks as needed.

Prerequirements

Following tools should be installed in your machine to be able to run/develop the sample application:

Create the Application

I used ABP's startup template (http://www.aspnetboilerplate.com/Templates) to create a new web application named "Acme.SimpleTaskApp" (company name, "Acme" here, is optional while creating templates):

ABP startup template creation

It creates a layered solution as shown below: 

Startup template projects

It includes 6 projects starting with the name that I entered as the project name:

  • .Core project is for domain/business layer (entities, domain services...)
  • .Application project is for application layer (DTOs, application services...)
  • .EntityFramework project is for EF Core integration (abstracts EF Core from other layers).
  • .Web project is for ASP.NET MVC layer.
  • .Tests project is for unit and integration tests (up to application layer, excluding web layer)
  • .Web.Tests project is for ASP.NET Core integrated tests (complete integration test for server side).

When you run the application, you can see the user interface of the template:

Template Home Page

It contains a top menu, empty Home and About pages and a language switch dropdown.

Developing the Application

Creating a Task Entity

I want to start with a simple Task entity. Since an entity is part of the domain layer, I added it into the .Coreproject:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Abp.Domain.Entities;
using Abp.Domain.Entities.Auditing;
using Abp.Timing;

namespace Acme.SimpleTaskApp.Tasks
{
    [Table("AppTasks")]
    public class Task : Entity, IHasCreationTime
    {
        public const int MaxTitleLength = 256;
        public const int MaxDescriptionLength = 64 * 1024; //64KB

        [Required]
        [MaxLength(MaxTitleLength)]
        public string Title { get; set; }

        [MaxLength(MaxDescriptionLength)]
        public string Description { get; set; }

        public DateTime CreationTime { get; set; }

        public TaskState State { get; set; }

        public Task()
        {
            CreationTime = Clock.Now;
            State = TaskState.Open;
        }

        public Task(string title, string description = null)
            : this()
        {
            Title = title;
            Description = description;
        }
    }

    public enum TaskState : byte
    {
        Open = 0,
        Completed = 1
    }
}


  • I derived from ABP's base Entity class, which includes Id property as int by default. We can use the generic version, Entity<TPrimaryKey>, to choice a different PK type.
  • IHasCreationTime is a simple interface just defines CreationTime property (it's good to use a standard name for CreationTime).
  • Task entity defines a required Title and an optional Description.
  • TaskState is a simple enum to define states of a Task.
  • Clock.Now returns DateTime.Now by default. But it provides an abstraction, so we can easily switch to DateTime.UtcNow in the feature if it's needed. Always use Clock.Now instead of DateTime.Now while working with ABP framework.
  • I wanted to store Task entities into AppTasks table in the database.

Adding Task to DbContext

.EntityFrameworkCore project includes a pre-defined DbContext. I should add a DbSet for the Task entity into the DbContext:

public class SimpleTaskAppDbContext : AbpDbContext
{
    public DbSet<Task> Tasks { get; set; }

    public SimpleTaskAppDbContext(DbContextOptions<SimpleTaskAppDbContext> options) 
        : base(options)
    {

    }
}


Now, EF Core knows that we have a Task entity. 

Creating the First Database Migration 

We will create an initial migration to create database and the AppTasks table. I open the Windows Command Line and run the following command (active directory should be the root directory of .EntityFrameworkCore project):

EF Core Initial Migration

This command creates a Migrations folder in the .EntityFrameworkCore project which includes a migration class and a snapshot of our database model:

EntityFrameworkCore Project Migrations 

Automatically generated "Initial" migration class is shown below:

public partial class Initial : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "AppTasks",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                CreationTime = table.Column<DateTime>(nullable: false),
                Description = table.Column<string>(maxLength: 65536, nullable: true),
                State = table.Column<byte>(nullable: false),
                Title = table.Column<string>(maxLength: 256, nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_AppTasks", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "AppTasks");
    }
}


This code is used to create AppTasks table when we execute the migrations to the database (see entity framework documentation for more information on migrations).

Creating the Database

To create the database, I run the following command in the command line:

dotnet ef database update

This command created a database named SimpleTaskAppDb in the local SQL Server and executed migrations (currently, there is a single, "Initial", migration):

Created Database

Now, I have a Task entity and corresponding table in the database.  I entered a few sample Tasks to the table:

AppTasks table

Note that, the database connection string is defined in appsettings.json in the .Web application.

Task Application Service

Application Services are used to expose domain logic to the presentation layer. An Application Service is called from presentation layer with a Data Transfer Object (DTO) as parameter (if needed), uses domain objects to perform some specific business logic and returns a DTO back to the presentation layer (if needed).

I'm creating the first application service, TaskAppService, into the .Application project to perform task related application logic. First, I wanted to define an interface for the app service:

public interface ITaskAppService : IApplicationService
{
    Task<ListResultOutput<TaskListDto>> GetAll(GetAllTasksInput input);
}

Defining an interface is not required, but suggested. By convention, all app services should implement IApplicationService interface in ABP. I just created a GetAll method to query tasks. To do that, I also defined the following DTOs:

public class GetAllTasksInput
{
    public TaskState? State { get; set; }
}

[AutoMapFrom(typeof(Task))] public class TaskListDto : EntityDto, IHasCreationTime { public string Title { get; set; } public string Description { get; set; } public DateTime CreationTime { get; set; } public TaskState State { get; set; } }
  • GetAllTasksInput DTO defines input parameters of the GetAll app service method. Instead of directly defining the state as method parameter, I added it into a DTO object. Thus, I can add other parameters into this DTO later without breaking my existing clients (we could directly add a state parameter to the method).
  • TaskListDto is used to return a Task data. It's derived from EntityDto, which just defines an Idproperty (we could add Id to our Dto and not derive from EntityDto). We defined [AutoMapFrom] attribute to create AutoMapper mapping from Task entity to TaskListDto. This attribute is defined in Abp.AutoMapper nuget package.
  • Lastly, ListResultOutput is a simple class contains a list of items (we could directly return a List<TaskListDto>).

Now, we can implement the ITaskAppService as shown below:

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Abp.Application.Services.Dto;
using Abp.Domain.Repositories;
using Abp.Linq.Extensions;
using Acme.SimpleTaskApp.Tasks.Dtos;
using Microsoft.EntityFrameworkCore;

namespace Acme.SimpleTaskApp.Tasks
{
    public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService
    {
        private readonly IRepository<Task> _taskRepository;

        public TaskAppService(IRepository<Task> taskRepository)
        {
            _taskRepository = taskRepository;
        }

        public async Task<ListResultOutput<TaskListDto>> GetAll(GetAllTasksInput input)
        {
            var tasks = await _taskRepository
                .GetAll()
                .WhereIf(input.State.HasValue, t => t.State == input.State.Value)
                .OrderByDescending(t => t.CreationTime)
                .ToListAsync();

            return new ListResultOutput<TaskListDto>(
                ObjectMapper.Map<List<TaskListDto>>(tasks)
            );
        }
    }
}


  • TaskAppService is derived from SimpleTaskAppAppServiceBase included in the startup template (which is derived from ABP's ApplicationService class). This is not required, app services can be plain classes. But ApplicationService base class has some pre-injected services (like ObjectMapper used here).
  • I used dependency injection to get a repository.
  • Repositories are used to abstract database operations for entities. ABP creates a pre-defined repository (like IRepository<Task> here) for each entity to perform common tasks. IRepository.GetAll()used here returns an IQueryable to query entities.
  • WhereIf is an extension method of ABP to simplify conditional usage of IQueryable.Where method.
  • ObjectMapper (which somes from the ApplicationService base class and implemented via AutoMapper by default) is used to map list of Task objects to list of TaskListDtos objects.

Testing the TaskAppService

Before going further to create user interface, I want to test TaskAppService. You can skip this section if you don't interest in automated testing.

Startup template contains .Tests project to test our code. It uses EF Core's InMemory database provider instead of SQL Server. Thus, our unit tests can work without a real database. It creates a separated database for each test. Thus, tests are isolated from each other. We can use TestDataBuilder class to add some initial test data to InMemory database before running tests. I changed TestDataBuilder as shown below:

public class TestDataBuilder
{
    private readonly SimpleTaskAppDbContext _context;

    public TestDataBuilder(SimpleTaskAppDbContext context)
    {
        _context = context;
    }

    public void Build()
    {
        _context.Tasks.AddRange(
            new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality."),
            new Task("Clean your room") { State = TaskState.Completed }
            );
    }
}


You can see the sample project's source code to understand where and how TestDataBuilder is used. I added two tasks (one of them is completed) to the dbcontext. So, I can write my tests assuming that there are two Tasks in the database. My first integration test tests the TaskAppService.GetAll method we created above.

public class TaskAppService_Tests : SimpleTaskAppTestBase
{
    private readonly ITaskAppService _taskAppService;

    public TaskAppService_Tests()
    {
        _taskAppService = Resolve<ITaskAppService>();
    }

    [Fact]
    public async System.Threading.Tasks.Task Should_Get_All_Tasks()
    {
        //Act
        var output = await _taskAppService.GetAll(new GetAllTasksInput());

        //Assert
        output.Items.Count.ShouldBe(2);
    }

    [Fact]
    public async System.Threading.Tasks.Task Should_Get_Filtered_Tasks()
    {
        //Act
        var output = await _taskAppService.GetAll(new GetAllTasksInput { State = TaskState.Open });

        //Assert
        output.Items.ShouldAllBe(t => t.State == TaskState.Open);
    }
}


I created two different tests to test GetAll method as shown above. Now, I can open Test Explorer (from Test\Windows\Test Explorer in the main menu of VS) and run the unit tests:

Test explorer

All of them succeed. The last one was a pre-built test in the startup template, which we can ignore for now.

Notice that; ABP startup template comes with xUnit and Shouldly installed by default. So, we used them to write our tests.

Task List View

Now, I know that TaskAppService is properly working. I can start to create a page to list all tasks.

Adding a New Menu Item

 First, I'm adding a new item to the top menu:

public class SimpleTaskAppNavigationProvider : NavigationProvider
{
    public override void SetNavigation(INavigationProviderContext context)
    {
        context.Manager.MainMenu
            .AddItem(
                new MenuItemDefinition(
                    "Home",
                    L("HomePage"),
                    url: "",
                    icon: "fa fa-home"
                    )
            ).AddItem(
                new MenuItemDefinition(
                    "About",
                    L("About"),
                    url: "Home/About",
                    icon: "fa fa-info"
                    )
            ).AddItem(
                new MenuItemDefinition(
                    "TaskList",
                    L("TaskList"),
                    url: "Tasks",
                    icon: "fa fa-tasks"
                    )
            );
    }

    private static ILocalizableString L(string name)
    {
        return new LocalizableString(name, SimpleTaskAppConsts.LocalizationSourceName);
    }
}


Startup template comes with two pages: Home and About, as shown above. We can change them or create new pages. I prefered to leave them for now and create a new menu item.

Creating the TaskController and ViewModel

I'm creating a new controller class, TasksController, in the .Web project as shown below:

public class TasksController : SimpleTaskAppControllerBase
{
    private readonly ITaskAppService _taskAppService;

    public TasksController(ITaskAppService taskAppService)
    {
        _taskAppService = taskAppService;
    }

    public async Task<ActionResult> Index(GetAllTasksInput input)
    {
        var output = await _taskAppService.GetAll(input);
        var model = new IndexViewModel(output.Items);
        return View(model);
    }
}


  • I derived from SimpleTaskAppControllerBase (which is derived from AbpController) that contains common base code for Controllers in this application.
  • I injected ITaskAppService in order to get list of tasks.
  • Instead of directly passing result of the GetAll method to the view, I created an IndexViewModel class in the .Web project which is shown below:
public class IndexViewModel
{
    public IReadOnlyList<TaskListDto> Tasks { get; }

    public IndexViewModel(IReadOnlyList<TaskListDto> tasks)
    {
        Tasks = tasks;
    }

    public string GetTaskLabel(TaskListDto task)
    {
        switch (task.State)
        {
            case TaskState.Open:
                return "label-success";
            default:
                return "label-default";
        }
    }
}


This simple view model gets a list of tasks (which is provided from ITaskAppService) in it's constructor. It also has GetTaskLabel method that will be used in the view to select a Bootstrap label class for given task.

Task List Page

And finally, the Index view is shown below:

@model Acme.SimpleTaskApp.Web.Models.Tasks.IndexViewModel

@{
    ViewBag.Title = L("TaskList");
    ViewBag.ActiveMenu = "TaskList"; //Matches with the menu name in SimpleTaskAppNavigationProvider to highlight the menu item
}

<h2>@L("TaskList")</h2>

<div class="row">
    <div>
        <ul class="list-group">
            @foreach (var task in Model.Tasks)
            {
                <li class="list-group-item">
                    <span class="pull-right label @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span>
                    <h4 class="list-group-item-heading">@task.Title</h4>
                    <div class="list-group-item-text">
                        @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss")
                    </div>
                </li>
            }
        </ul>
    </div>
</div>


We simply used given model to render the view using Bootstrap's list group component. Here, we used IndexViewModel.GetTaskLabel() method to get label types for tasks. Rendered page will be like that:

Task list

Localization

We used L method in the view which comes from ABP framework. It's used to localize strings. We have define localized strings in Localization/Source folder in the .Core project as .json files. English localization is shown below:

{
  "culture": "en",
  "texts": {
    "HelloWorld": "Hello World!",
    "ChangeLanguage": "Change language",
    "HomePage": "HomePage",
    "About": "About",
    "Home_Description": "Welcome to SimpleTaskApp...",
    "About_Description": "This is a simple startup template to use ASP.NET Core with ABP framework.",
    "TaskList": "Task List",
    "TaskState_Open": "Open",
    "TaskState_Completed": "Completed"
  }
}


Most of the texts are coming from startup template and can be deleted. I just added the last 3 lines and used in the view above. While using ABP's localization is pretty simple, you can see localization document for more information on the localization system.

Filtering Tasks

As shown above, TaskController actually gets a GetAllTasksInput that can be used to filter tasks. So, we can add a dropdown to task list view to filter tasks. First, I added the dropdown to the view (I added inside the header):

<h2>
    @L("TaskList")
    <span class="pull-right">
        @Html.DropDownListFor(
           model => model.SelectedTaskState,
           Model.GetTasksStateSelectListItems(LocalizationManager),
           new
           {
               @class = "form-control",
               id = "TaskStateCombobox"
           })
    </span>
</h2>


Then I changed IndexViewModel to add SelectedTaskState property and GetTasksStateSelectListItems method:

public class IndexViewModel
{
    //...

    public TaskState? SelectedTaskState { get; set; }

    public List<SelectListItem> GetTasksStateSelectListItems(ILocalizationManager localizationManager)
    {
        var list = new List<SelectListItem>
        {
            new SelectListItem
            {
                Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, "AllTasks"),
                Value = "",
                Selected = SelectedTaskState == null
            }
        };

        list.AddRange(Enum.GetValues(typeof(TaskState))
                .Cast<TaskState>()
                .Select(state =>
                    new SelectListItem
                    {
                        Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, $"TaskState_{state}"),
                        Value = state.ToString(),
                        Selected = state == SelectedTaskState
                    })
        );

        return list;
    }
}


We should set SelectedTaskState in the controller:

public async Task<ActionResult> Index(GetAllTasksInput input)
{
    var output = await _taskAppService.GetAll(input);
    var model = new IndexViewModel(output.Items)
    {
        SelectedTaskState = input.State
    };
    return View(model);
}


Now, we can run the application to see the combobox at the top right of the view:

Task list

I added the combobox but it can not work yet. I'll write a simple javascript code to re-request/refresh task list page when combobox value changes. So, I'm creating wwwroot\js\views\tasks\index.js file in the .Web project:

(function ($) {
    $(function () {

        var _$taskStateCombobox = $('#TaskStateCombobox');

        _$taskStateCombobox.change(function() {
            location.href = '/Tasks?state=' + _$taskStateCombobox.val();
        });

    });
})(jQuery);


Before including this javascript file into my view, I used Bundler & Minifier VS extension (which is default way of minifying files in ASP.NET Core projects) to minify the script:

Minify js

This adds the following lines into bundleconfig.json file in the .Web project:

{
  "outputFileName": "wwwroot/js/views/tasks/index.min.js",
  "inputFiles": [
    "wwwroot/js/views/tasks/index.js"
  ]
}


And creates a minified version of the script:

Minified js file

Whenever I change the index.js, index.min.js is automatically re-generated. Now, I can include the javascript file into my page:

@section scripts
{
    <environment names="Development">
        <script src="~/js/views/tasks/index.js"></script>
    </environment>

    <environment names="Staging,Production">
        <script src="~/js/views/tasks/index.min.js"></script>
    </environment>
}


With this code, our view will use index.js in development and index.min.js (minified version) in production. This is a common approach in ASP.NET Core MVC projects.

Automated Testing Task List Page

We can create integration tests that is also integrated to ASP.NET Core MVC infrastructure. Thus, we can completely test our server side code. You can skip this section if you don't interest in automated testing.

ABP startup template includes a .Web.Tests project to do that. I created a simple test to request to TaskController.Index and check the response:

public class TasksController_Tests : SimpleTaskAppWebTestBase
{
    [Fact]
    public async System.Threading.Tasks.Task Should_Get_Tasks_By_State()
    {
        //Act

        var response = await GetResponseAsStringAsync(
            GetUrl<TasksController>(nameof(TasksController.Index), new
                {
                    state = TaskState.Open
                }
            )
        );

        //Assert

        response.ShouldNotBeNullOrWhiteSpace();
    }
}


GetResponseAsStringAsync and GetUrl methods are some helper methods provided by AbpAspNetCoreIntegratedTestBase class of ABP. We can instead directly use the Client (an instance of HttpClient) property to make requests. But using these shortcut methods makes it easier. See integration testing documentation of ASP.NET Core for more.

When I debug the test, I can see the response HTML:

Web test

That shows the Index page returned a response without any exception. But... we may want to go more and check if returned HTML is what we expect. There are some libraries can be used to parse HTML. AngleSharp is one of them and comes as pre-installed in ABP startup template's .Web.Tests project. So, I used it to check the created HTML code:

public class TasksController_Tests : SimpleTaskAppWebTestBase
{
    [Fact]
    public async System.Threading.Tasks.Task Should_Get_Tasks_By_State()
    {
        //Act

        var response = await GetResponseAsStringAsync(
            GetUrl<TasksController>(nameof(TasksController.Index), new
                {
                    state = TaskState.Open
                }
            )
        );

        //Assert

        response.ShouldNotBeNullOrWhiteSpace();

        //Get tasks from database
        var tasksInDatabase = await UsingDbContextAsync(async dbContext =>
        {
            return await dbContext.Tasks
                .Where(t => t.State == TaskState.Open)
                .ToListAsync();
        });

        //Parse HTML response to check if tasks in the database are returned
        var document = new HtmlParser().Parse(response);
        var listItems = document.QuerySelectorAll("#TaskList li");
            
        //Check task count
        listItems.Length.ShouldBe(tasksInDatabase.Count);

        //Check if returned list items are same those in the database
        foreach (var listItem in listItems)
        {
            var header = listItem.QuerySelector(".list-group-item-heading");
            var taskTitle = header.InnerHtml.Trim();
            tasksInDatabase.Any(t => t.Title == taskTitle).ShouldBeTrue();
        }
    }
}


You can check the HTML deeper and in more detailed. But in most cases, checking the fundamental tags will be enough.

LINK: https://www.codeproject.com/articles/1115763/using-asp-net-core-entity-framework-core-and-asp-n