0

Native Html Push Notification With Asp.Net API

Hôm nay mình sẽ xây dựng một ví dụ nhỏ về native HTML5 đẩy notification lên WebAPI, nó có thể xem như là một chatbox đơn giản. Hy vọng sẽ hữu ích cho mọi người. Chúng ta sẽ xây dựng một page cho phép nhiều người chat cùng nhau, và chúng ta cũng có thể dựa trên những điều này để xây dựng nên các phần phức tạp hơn.

Building the project

Sever Code

Bây giờ chúng ta sẽ đi tạo project Asp.Net MVC với template là Web API Đầu tiên thêm một Model gọi là Message.cs

public class Message
{
   public string username { get; set; }
   public string text { get; set; }
   public string dt { get; set; }
}

model này sẽ được binding (truyền) vào trong ứng dụng chat của chúng ta. Bất cứ khi nào bạn submit( gửi) một tin nhắn, và phương thức post trong controller sẽ nhận dữ liệu input vào.

Tiếp theo chúng ta tạo controller ChatController.cs.

public class ChatController : ApiController
{
        public HttpResponseMessage Get(HttpRequestMessage request)
        {
           HttpResponseMessage response = request.CreateResponse();
           response.Content = new PushStreamContent(OnStreamAvailable, "text/event-stream");
           return response;
        }
        public void Post(Message m)
        {
            m.dt = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss");
            MessageCallback(m);
        }
}

Trong này sẽ có 2 phương thức chính Get và Post. Phương thức Get sẽ được gọi bởi clients để đăng kí đến EventSource stream, và Post sẽ có nhiệm vụ gửi message chat. Ở đây khi người dùng tạo request get, chúng ta sẽ tạo ra response message bằng việc sử dụng PushStreamContent, đối tượng này sẽ đẩy tham số onStreamAvailable vào trong hàm dựng và cho phép trả về response stream.

private static readonly ConcurrentQueue<StreamWriter> _streammessage = new ConcurrentQueue<StreamWriter>();
public static void OnStreamAvailable(Stream stream, HttpContentHeaders headers, TransportContext context)
{
  StreamWriter streamwriter = new StreamWriter(stream);
  _streammessage.Enqueue(streamwriter);
}
 private static void MessageCallback(Message m)
{
  foreach (var subscriber in _streammessage)
  {
    subscriber.WriteLine("data:" + JsonConvert.SerializeObject(m) + "n");
    subscriber.Flush();
   }
}

Hàm OnStreamAvailable của chúng ta ở đây, các bạn có thể thấy mỗi khi phương thức Get được gọi thì nó sẽ lấy nội dung message được chứa trong ConcurrentQueue<StreamWriter> để đưa ra response. Về ConcurrentQueue đây là một thread-safe với cơ chế first in first out. Và khi người dugnf tạo phương thức Post Request, chúng ta sử dụng model binding để đẩy message object từ request và truyền nó đến các subcriber đang đăng kí trong ConcurrentQueue thông qua funtion MessageCallback OnStreamAvailable sẽ khởi tạo mới một StreamWriter cho mỗi người mới( người tham gia chát trong chatbox). Như đã nói nói sẽ được đẩy vào trong ConcurrentQueue tĩnh chúng ta gọi là streammessage. Nó sẽ được chia sẽ với tất cả các đối tượng trong controlller và cho phép chúng ta viết các thông báo mới và đẩy về cho mọi subcriber trong box. Bạn có thể thấy chúng ta sẽ cập nhật đấy các thông báo message mới xuống các subcriber mỗi khi hàm post message được dùng bằng việc gọi lại phương thức MessageCallback. Như thế là đã xong với cơ chế làm việc trong API của chúng ta, giờ đến việc làm thế nào để get và push messager lên API và lắng nghe liên tục khi có message mới từ subcriber khác trong box gửi lên.

Client Side Code

Ở đây mình sẽ sử dụng với knockout.js

    <script src="@Url.Content("~/Scripts/knockout.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/knockout.mapping-latest.js")" type="text/javascript"></script>

Phần HTML chúng ta sẽ tạo với form đơn giản như sau

<body>
    <header>
        <div class="content-wrapper">       
            <div class="float-left">
                <p class="site-title"><a href="/">ASP.NET Web API</a></p>
            </div>
        </div>
    </header>
    <div id="body">
        <section class="content-wrapper main-content clear-fix">

                <div id="console" data-bind="template: {name: 'chatMessageTemplate', foreach: chatMessages, afterAdd: resizeChat}">
                </div>
                
                <label for="username">Enter username to start chatting</label>
                <input type="text" id="username" data-bind="value: chatUsername" />

                <div id="chatControl" value="send" data-bind="visible: chatUsername().length > 0">
                    <textarea type="text" id="push" data-bind="value: newMessage.text"></textarea>
                    <button id="pushbtn">Send</button>
                </div>

                <script type="text/html" id="chatMessageTemplate">
                    <p><small data-bind="text: dt"></small><br/>
                    <strong data-bind="text: username"></strong>: <i data-bind="text: text"></i></p>
                </script>

        </section>
    </div>
</body>

Div Id="console" nó là thẻ để show chat của các subcriber trong box với dữ liệu bind vào theo template là Json object với các thuôc tính username, message body, và ngày tạo message. các tag username, message và button submit sẽ là form để submit message của bạn lên chat box.

ViewModel

chúng ta sẽ có một viewmodel để thao tác xử lí từ object message gốc và các thuộc tính ngoài cần thiết để đưa ra view cũng như phục vụ cho việc submit message mới như sau

var viewModel = {
  chatMessages: ko.observableArray([]),
  chatUsername: ko.observable("")
}
viewModel.newMessage = {
  username: ko.computed(function () { return viewModel.chatUsername() }),
  text: ko.observable("")
}
viewModel.resizeChat = function () {
  $("#console").scrollTop($("#console")[0].scrollHeight);
};

Riêng resizeChat đây là method được gọi sau khi binding dữ liệu vào chat template, sau khi tất cả message xuất hiện lên chatbox thì chúng ta cần scroll hộp chatbox xuống cuối để thấy được latest message. Và nó sẽ được binding vào view khi doccument đã được load hoàn tất.

$(document).ready(function () {
    ko.applyBindings(viewModel);
});

Sự kiện post và get gọi ở client-side

Phần cuối cùng và cũng là thú vị nhất là việc sử dụng HTML5 EventSource để lắng nghe dữ liệu message được đẩy xuống từ sever. Chúng ta sẽ xử lí điều đó nhưng trước tiên cần phải add hàm để gửi message lên sever khi nhấn vào button submit

$("#pushbtn").click(function () {
    $.ajax({
      url: "http://localhost:49999/api/chat/",
      data: JSON.stringify(ko.mapping.toJS(viewModel.newMessage)),
      cache: false,
      type: 'POST',
      dataType: "json",
      contentType: 'application/json; charset=utf-8'
});
    viewModel.newMessage.text('');
    $("#push").val('');
});

Ở đây khi bạn click vào button submit nó sẽ gửi post request lên API, về message đã được bound với knockout.js vì thế chúng ta sẽ không cần get thêm dữ liệu nào nữa, việc cần làm chỉ là sử dụng knockout.js để map plugin đến đối tượng cần được đổ dữ liệu từ knockout.js object. Cách đơn giản đấy là xóa message box và message text từ viewModel. Tiếp đến EventSource, nó sẽ làm việc như là một cơ chế đăng kí theo dõi. Và được add vào trong $(document).ready()

if (!!window.EventSource) {
   var source = new EventSource('http://localhost:49999/api/chat/');
   source.addEventListener('message', function (e) {
        var json = JSON.parse(e.data);
        viewModel.chatMessages.push(json);
   }, false);
   source.addEventListener('open', function (e) {
     console.log("open!");
   }, false);
  source.addEventListener('error', function (e) {
    if (e.readyState == EventSource.CLOSED) {
         console.log("error!");
    }
  }, false);
} else {
  // not supported!
}

Đầu tiên chúng ta cần check trình duyệt có hỗ trợ EventSource, nếu có chúng ta tạo một EventSource mới để truyền vào url đến chat controller. EventSource với phương thức mặt định là Get, và nó mở kết nối với context type là "text/event-stream". Tiếp theo đó chúng ta cần xử lí thêm trong các sự kiện của nó như trên. Trong sự kiện onmessage event của EventSource, khi nó được tạo ra chúng ta sẽ lấy response từ server và parse qua JSON và đẩy nó vào viewModel.chatmessages, như thế nó sẽ cập nhật lại list message chat trên UI. Ở đây mục else, sẽ xử lí nếu browser không hỗ trộ EventSource như sau

            console.log("xdR");
            var xdr = new XDomainRequest();
            xdr.open("GET", "/api/chat/?" + Math.random());
            xdr.send();
            xdr.onload = function () {
                console.log(xdr);
                var data = JSON.parse(xdr.responseText);
                viewModel.chatMessages.push(data);
            }
            xdr.onprogress = function () {
                console.log(xdr.responseText);
                var data = JSON.parse(xdr.responseText);
                viewModel.chatMessages.push(data);
            }

Chúng ta sẽ tạo đối tượng XDomainRequest thay vì EventSource


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí