SignalRとTypeScriptとknockout.jsで簡単なチャットを作成

最近の.netは素晴らしい!!

ずっと封鎖的だった.net環境。
最近は、超オープンですね。C#を愛する人間としては、嬉しい限りです。

SignalRで簡単なチャット

とりあえず、クライアントが文章を入力して送信したら
リアルタイムで配信されるだけの簡単なチャットの作成方法をご紹介します。

環境

技術的な話

SignalRの話を追い求めてこのブログに辿り着いたあなたも、
きっと.net大好きなハズなので、 以下の技術を使用します。

  • SignalR
  • TypeScript
  • Web API
  • knockout.js (個人的に大好き)

スタート

では、早速
新しいプロジェクト > Web > ASP.NET Web アプリケーション
を作成しましょう。

名前は適当に『MyChat』にしておきます。

テンプレートは、Emptyを選んで、Web APIにチェックをいれておきましょう。

そしたら、NuGetで以下を取得します。

  • knockoutjs
  • knockout.TypeScript.DefinitelyTyped
  • jquery.TypeScript.DefinitelyTyped
  • signalr.TypeScript.DefinitelyTyped
  • Bootstrap

取得できたら、SignalR ハブクラス(v2)を追加します。
名前は、ChatHub.csにしておきましょう。
デフォルトでHelloというメソッドがありますが、消しちゃいましょう。
そして、using Microsoft.AspNet.SignalR.Hubs;を追加して、
HubNameという属性を付加します。
この属性は、jsサイドでHubの扱いが楽になるので必須です。

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace MyChat
{
    [HubName("ChatHub")]
    public class ChatHub : Hub
    {
    }
}

いい感じです。
混乱するかもしれませんが、以降はこのHubに編集は行いません。

次に、Hubのラッパーを作成します。
なんでもいいんだけど、ChatHubHandlerllerというクラスを追加します。
こいつが、Hubを使ってチャットに新規メッセージが追加された事を
クライアントに送信します。

using Microsoft.AspNet.SignalR;

namespace MyChat
{
    public class ChatHubHandlerller
    {
        /// <summary>
        /// クライアントに新規投稿メッセージを通知します。
        /// </summary>
        /// <param name="msg">新規投稿メッセージ</param>
        public void NotifyChatAdd(string msg)
        {
            var context = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
            context.Clients.All.SubscribeChatAdd(msg.Replace("\n", "<br />")); // とりあえず、改行だけ置換
        }
    }
}

SubscribeChatAdd?
と思われるかもしれませんが、こいつは動的な式なので
実行時に解決されます。

実は、SignalRはこれでは動きません。
Owinを使用して立ち上げる必要があります。
StartUp.csというクラスを追加して下さい。

using Microsoft.Owin;
using Owin;

[assembly: OwinStartupAttribute(typeof(MyChat.StartUp))]
namespace MyChat
{
    public partial class StartUp
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

partialなのに、注意!!

次に、Viewから呼び出されるAPIを作成しましょう。
ChatV1Controllerという名前で、Web API コントローラー クラス(v2.1)
を追加します。
今回は、メッセージの新規投稿できれば良いので、Get等は消しておきます。

一応、属性マッピングを使ってAPIのバージョニングを可能にしておきます。
属性ルーティングしたりして、以下の形にして下さい。

using System.Web.Http;

namespace MyChat
{
    public class ChatV1Controller : ApiController
    {

        [Route("api/v1/chat")]
        public void Post([FromBody]string value)
        {
            var chatHub = new ChatHubHandlerller();
            chatHub.NotifyChatAdd(value);
        }

    }
}

/api/v1/chat
でアクセスできるようになります。
こうしておくことで、APIのバージョンを共存させられます。

Web API: The Good Parts、凄く良い本でした。

Web API: The Good Parts

Web API: The Good Parts

お次に、knockout.jsを使用してViewModelを作ります。 Scriptの中に、viewmodelsというフォルダを作成して、
ChatViewModel.tsというTypeScriptを作成しましょう。

/// <reference path="../typings/knockout/knockout.d.ts" />
/// <reference path="../typings/jquery/jquery.d.ts" />
/// <reference path="../typings/signalr/signalr.d.ts" />

/**
 * Chat機能を提供するViewModelです
 */
class ChatViewModel {

    /**
    * ViewModelのバインドを開始します。
    */
    public static StartBinding(elementName: string): void {
        var _vm = new ChatViewModel();
        ko.applyBindings(_vm, $(elementName)[0]);
    }

    /**
     * メッセージ一覧を格納します。
     */
    public ChatMessages: KnockoutObservableArray<string> = ko.observableArray([]);
    /**
     * ユーザのインプットメッセージです。
     */
    public InputMessage: KnockoutObservable<string> = ko.observable("");
    /**
     * SingalRの接続状況のメッセージです。
     */
    public SingalRConnectStateMsg: KnockoutObservable<string> = ko.observable("SingalR未接続");

    // SignalR Hub
    public _Hub: HubProxy = null;

    constructor() {
        this._Hub = $.connection.hub.createHubProxy('ChatHub');

        // チャットメッセージ配信を購読
        this._Hub.on('SubscribeChatAdd', (msg) => {
            this.ChatMessages.push(msg);
            this.InputMessage("");
        });

        // 接続待機
        $.connection.hub.start().done(() => {
            this.SingalRConnectStateMsg("SingalR接続済み");
        });
    }

    /**
     * メッセージの投稿を実行します。
     */
    public PostMessage(): void {
        var self = this;
        $.ajax({
            type: "POST",
            url: "/api/v1/chat",
            contentType: "application/json",
            data: JSON.stringify(self.InputMessage()),
            success: function () { }
        });
    }
}

続いて、View。
Viewに関しては、Razorみたいに動的でも良いですし、
静的なhtmlでも良いです。
今回はhtmlで。 index.htmlを追加しましょう。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <link href="Content/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div id="chat-wrap" style="width: 500px; margin: 0 auto;">
        <h2>My Chat</h2>

        <p id="connection-satus" data-bind="text: SingalRConnectStateMsg"></p>

        <div id="chat_messages" data-bind="template: { name: 'chatMsgTmpl', foreach: ChatMessages }"></div>
        <script id="chatMsgTmpl" type="text/html">
            <div class="panel panel-default">
                <div class="panel-body" data-bind="html: $data">
                </div>
            </div>
        </script>

        <div class="text-center">
            <div style="margin-bottom: 11px;">
                <textarea id="txtChatInput" class="form-control" data-bind="value: InputMessage" rows="5"></textarea>
            </div>
            <button id="btnPostChat" type="button" class="btn btn-primary" data-bind="click: PostMessage">送信</button>
        </div>
    </div>
    <script src="Scripts/jquery-1.10.2.min.js"></script>
    <script src="Scripts/knockout-3.2.0.js"></script>
    <script src="Scripts/bootstrap.min.js"></script>
    <script src="Scripts/jquery.signalR-2.1.2.min.js"></script>
    <script src="/signalr/hubs"></script>
    <script src="Scripts/viewmodels/ChatViewModel.js"></script>
    <script>
        $(function () {
            var _vm = new ChatViewModel();
            ko.applyBindings(_vm, $("#chat-wrap")[0]);
        });
    </script>
</body>
</html>

scriptのバージョンは、各自のバージョンに合わせてくださいね。 それでは、実行してみましょう!

他のブラウザから同じURLにアクセスしてみて、投稿ボタンを押してみましょう。
リアルタイムで内容が反映されます。
すげぇぇぇぇぇえええ!