SPA(Single Page Application) for knockout.js + TwitterBootstrap + Sammy.js + SQL Azure + Visual Studio2013

概要

Mobile(携帯)、PC/Tabletでも動作し、かつローカルアプリのように”さくさく”動かすためにには JavaScriptでSPAで、一生懸命かかないといけないです。

今回、簡単にSPA(Singe Page Application)ができるよに再利用可能な ベースクラスを作成してみました。

MITライセンスでご自由に改変して使ってください。

以下のサイトを事前にみておくと理解が早いです。

要件

  1. iPhone/Android/PC/Tabletで動作すること
    1. レシンポンシブルデザインにするためTwitterBootstrapを使用し各種端末に対応する。
    2. SPAの実現のために、JavaScriptを使用する
    3. Jquery + Knockout.js を使用して各種端末に対応する。
  2. 戻るボタン等のURL履歴に対応のこと
    1. Sammy.js により、URL履歴に対応する。
  3. デバックしやすくするためにVisual Studio 2013を使用する
  4. データベースはSQL Azureを使用する。
  5. WebサイトはWindows Azure 簡易Webサイトを使用する。

Demo Web Site(デモサイト)

http://vs-samples.azurewebsites.net/Home/SPA010

Program Source(プログラムソース)

サンプルコードは以下のGitHubに保存しました。

https://github.com/snoro/VS2013_MVC_Sample/tree/master/Backborn_Todos_WebAPI

イメージ

一覧画面

新規作成をクリックすると新規作成画面へ

明細をクリックすると更新画面へ

MVC5_VS2013_Knockout_SPA_0010

新規作成画面

MVC5_VS2013_Knockout_SPA_0020

更新画面

MVC5_VS2013_Knockout_SPA_0030

課題

  1. データの読み込み時、更新時にLoading画面を表示してボタンを押せないようにする。
  2. 一覧画面にページング処理をいれる。
  3. 明細データの取得や設定のコードを書かないでいいように改良する。
  4. 入力時に日付等にデートピッカーをつける

ソース抜粋

SPA010.cshtml.cshtml

@{
 ViewBag.Title = "Home Page";
}
<div id="disp" style="display:none">

<!--ko if: koSPAIsVisibleList -->
<div class="header">
 <h2>一覧</h2>
 <button type="button" class="btn btn-primary" data-bind="click: loadDetails">
 再読込
 </button>

<button type="button" class="btn btn-warning" data-bind="click: locationDetailCreate">
 新規作成
 </button>
 <br>
 <span class="label label-default">データ件数</span><span data-bind="text: koSPA_List().length"></span>
 <span class="label label-default">Message</span><span data-bind="text: koSPA_List_Message"></span>

</div>
<div class="results">
 <table class="table table-striped">
 <thead>
 <tr>
 <th>Id</th>
 <th>タイトル</th>
 <th>日付</th>
 <th></th>
 </tr>
 </thead>
 <tbody data-bind="foreach: koSPA_List">
 <tr data-bind="click: locationDetailUpdate">
 <td data-bind="text: Id"></td>
 <td data-bind="text: Title"></td>
 <td data-bind="text: Timestamp"></td>
 <td><span class="glyphicon glyphicon-chevron-right"></span></td>
 </tr>
 </tbody>
 </table>
</div>
<!--/ko-->

<!--ko if: koSPAIsVisibleDetail -->
<div class="header">
 <h2>詳細
 <!--ko if: koSPAIsVisibleDetail_Update -->
 (更新)
 <!--/ko-->
 <!--ko ifnot: koSPAIsVisibleDetail_Update -->
 (新規作成)
 <!--/ko-->
 </h2>

<button type="button" class="btn btn-primary" onClick="history.back(); return false;">
 <span class="glyphicon glyphicon-chevron-left"></span>戻る
 </button>
 <!--ko if: koSPAIsVisibleDetail_Update -->
 <button type="button" class="btn btn-warning" data-bind="click: updateDetailToDB">
 更新
 </button>
 <button type="button" class="btn btn-danger" data-bind="click: deleteDetailFromDB">
 削除
 </button>
 <!--/ko-->

<!--ko ifnot: koSPAIsVisibleDetail_Update -->
 <button type="button" class="btn btn-warning" data-bind="click: createDetailToDB">
 新規作成
 </button>
 <!--/ko-->

<br>
 <span class="label label-default">Message</span><span data-bind="text: koSPA_Detail_Message"></span>

</div>

<div class="results">

<table class="table">
 <thead>
 <tr>
 <th>項目名</th>
 <th>値</th>
 </tr>
 </thead>
 <tbody >
 <!--ko if: koSPAIsVisibleDetail_Update -->
 <tr>
 <td>Id</td>
 <td><span data-bind="text: koSPA_Detail.Id"></span></td>
 </tr>
 <!--/ko-->
 <tr>
 <td>Title</td>
 <td><input type="text" data-bind="value: koSPA_Detail.Title" /></td>
 </tr>
 <tr>
 <td>Detail</td>
 <td><input type="text" data-bind="value: koSPA_Detail.Content" /></td>
 </tr>
 <tr>
 <td>Timestamp</td>
 <td><input type="text" data-bind="value: koSPA_Detail.Timestamp" /></td>
 </tr>
 </tbody>
 </table>

</div>
<!--/ko-->

</div>

@section scripts{
 <script type="text/javascript" src="/Scripts/knockout-3.0.0.js"></script>
 <script type="text/javascript" src="/Scripts/sammy-0.7.4.js"></script>
 <script type="text/javascript" src="/Scripts/Users/knockout-SPA-0.1.1.js"></script>

<script type="text/javascript">
 $(document).ready(function () {
 //knockout SPAのパラメータを設定する
 //※このオブジェクトはNewする必要がないのでFunctionではない
 var koSPAParm = {
 url_List: "/api/Memo/",
 url_Detail_Create: "/api/Memo/",
 url_Detail_Update: "/api/Memo/",
 url_Detail_Delete: "/api/Memo/",

//Knockout Model
 koSPA_Model_Detail: function () {
 this.Id = ko.observable();
 this.Title = ko.observable();
 this.Content = ko.observable();
 this.Timestamp = ko.observable();
 },
 //Knockout View-Model
 koSPA_ViewModel : {
 koSPA_List: ko.observableArray(),
 koSPA_List_Message: ko.observable(""),
 koSPA_Detail: {
 Id: ko.observable(""),
 Title: ko.observable(""),
 Content: ko.observable(""),
 Timestamp: ko.observable(""),

 },
 koSPA_Detail_Message: ko.observable("")
 }

}
 //Using Post Detail to Database
 //when Detail-Create-Button is clicked
 //or Detail-Update-Button is clicked
 koSPAParm.koSPA_ViewModel.createDetailToObj = function () {
 var objDetail = {
 //ID不要
 Title: this.koSPA_Detail.Title(),
 Content: this.koSPA_Detail.Content(),
 Timestamp: this.koSPA_Detail.Timestamp()
 };

return objDetail
 }

//Using Initialize Detail
 //When List-Create-button is clicked
 koSPAParm.koSPA_ViewModel.initDetail = function () {
 this.koSPA_Detail.Id(-1);
 this.koSPA_Detail.Title("");
 this.koSPA_Detail.Content("");
 this.koSPA_Detail.Timestamp(new Date());
 }

//Using Set List to Detail
 //When List-Row is clicked
 koSPAParm.koSPA_ViewModel.setListToDetail = function (detailOfList) {
 this.koSPA_Detail.Id(detailOfList.Id());
 this.koSPA_Detail.Title(detailOfList.Title());
 this.koSPA_Detail.Content(detailOfList.Content());
 this.koSPA_Detail.Timestamp(detailOfList.Timestamp());

}
 //Using Set Detal to List
 //When Detail-Update-button is clicked
 koSPAParm.koSPA_ViewModel.setDetailToList = function (detailOfList) {
 detailOfList.Id(this.koSPA_Detail.Id());
 detailOfList.Title(this.koSPA_Detail.Title());
 detailOfList.Content(this.koSPA_Detail.Content());
 detailOfList.Timestamp(this.koSPA_Detail.Timestamp());
 }

//Using Add Detail to List
 //When List-Load-button is Clicked
 //or Detail-Create-button is clicked
 koSPAParm.koSPA_ViewModel.createDBDetailToObj = function (DBDetail) {
 var objDetail = new koSPAParm.koSPA_Model_Detail()
 .Id(DBDetail.Id)
 .Title(DBDetail.Title)
 .Content(DBDetail.Content)
 .Timestamp(DBDetail.Timestamp);
 return objDetail;

}
 //knockout SPAを実行する
 var koSPAApp = $.koSPA.CreateApp(koSPAParm);
 koSPAApp.run();
 document.getElementById("disp").style.display = "block";
 });
 </script>
}

 

knockout-SPA-0.1.1.js

/*
* Knockout SPA(Single Page Application)
* Created By Seiji Noro (https://github.com/snoro)
*
* Source:
* MIT License: http://www.opensource.org/licenses/MIT
*/
(function ($, window) {
 //knockout-3.0.0.jsでテスト済み
 if (typeof (ko) === undefined) { throw 'Knockout is required, please ensure it is loaded before loading this knockout-SPA'; }
 //sammy-0.7.4.jsでテスト済み
 if (typeof (Sammy) === undefined) { throw 'Sammy is required, please ensure it is loaded before loading this knockout-SPA'; }
 (function (factory) {
 //公開する関数をkoSPAだけにする

// Support module loading scenarios
 if (typeof define === 'function' && define.amd) {
 // AMD Anonymous Module
 define(['jquery'], factory);
 } else {
 // No module loader (plain <script> tag) - put directly in global namespace
 $.koSPA = window.koSPA = factory($);
 }
 })(function ($) {
 //koSPAオブジェクト
 var koSPA = {};
 koSPA.VERSION = '0.1.0';
 koSPA.location = window.location.toString();

//CreateApp
 koSPA.CreateApp = function () {
 var koSPAAppFunc = arguments[0];
 //koSPAApp オブジェクトをNewする。
 var koSPAApp = new koSPA.App(koSPAAppFunc);
 return koSPAApp;
 };

 //koSPAApp オブジェクト
 //初期値を設定するためにFunctionにしている。
 koSPA.App = function (koSPAParm)
 {
 //////////////////////////////////
 //knockout Model
 //////////////////////////////////
 //これはprototypeでないとメモリにコピーされるためよくない
 koSPAParm.koSPA_Model_Detail.prototype.locationDetailUpdate = function () {
 window.location.hash = "#/DetailUpdate/" + this.Id();
 };
 //////////////////////////////////
 //knockout View-Model
 //////////////////////////////////
 //thisに直接代入すると一覧取得のときにkoSPA_ViewModelを発見できない
 this.koSPA_ViewModel = koSPAParm.koSPA_ViewModel;

//knockout View-Model Property
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleList = ko.observable(true);
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail = ko.observable(true);
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail_Update = ko.observable(true);

&nbsp;

//knockout View-Model Method
 //(knockoutはViewModelのをprototypeで書くと、clickメソッド等でエラーにになる)
 koSPAParm.koSPA_ViewModel.loadDetails = function () {
 this.koSPA_List.removeAll(); // reset
 $.ajax({
 type: "GET",
 url: koSPAParm.url_List,
 dataType: "json"
 })
 .done(function (result) {
 $.each(result, function (i, p) {
 koSPAParm.koSPA_ViewModel.koSPA_List.push(
 koSPAParm.koSPA_ViewModel.createDBDetailToObj(p)
 );
 koSPAParm.koSPA_ViewModel.koSPA_List_Message("データを取得しました:" + new Date());
 });
 });
 };
 koSPAParm.koSPA_ViewModel.locationList = function () {
 window.location.hash = "#/List/";
 };
 koSPAParm.koSPA_ViewModel.locationDetailCreate = function () {
 window.location.hash = "#/DetailCreate";
 };
 koSPAParm.koSPA_ViewModel.createDetailToDB = function () {
 var dbDetailData = this.createDetailToObj();

$.ajax({
 type: "POST",
 url: koSPAParm.url_Detail_Create,
 contentType: "application/json;charset=UTF-8",
 dataType: "json",
 data: JSON.stringify(dbDetailData)
 })
 .done(function (result) {
 koSPAParm.koSPA_ViewModel.koSPA_List.push(
 koSPAParm.koSPA_ViewModel.createDBDetailToObj(result)
 );
 koSPAParm.koSPA_ViewModel.koSPA_Detail_Message("データを新規作成しました:" + new Date());
 })
 };
 koSPAParm.koSPA_ViewModel.updateDetailToDB = function () {
 var dbDetailData = this.createDetailToObj();

$.ajax({
 type: "POST",
 url: koSPAParm.url_Detail_Update + koSPAParm.koSPA_ViewModel.koSPA_Detail.Id(),
 contentType: "application/json;charset=UTF-8",
 dataType: "json",
 data: JSON.stringify(dbDetailData)
 })
 .done(function (result) {
 var detailOfList = koSPAParm.koSPA_ViewModel.findDetailFromList(koSPAParm.koSPA_ViewModel.koSPA_Detail.Id());
 koSPAParm.koSPA_ViewModel.setDetailToList(detailOfList)
 koSPAParm.koSPA_ViewModel.koSPA_Detail_Message("データを更新しました:" + new Date());
 })
 };

koSPAParm.koSPA_ViewModel.deleteDetailFromDB = function () {
 $.ajax({
 type: "DELETE",
 url: koSPAParm.url_Detail_Delete + koSPAParm.koSPA_ViewModel.koSPA_Detail.Id(),
 contentType: "application/json;charset=UTF-8",
 })
 .done(function (result) {
 koSPAParm.koSPA_ViewModel.removeDetailFromList(koSPAParm.koSPA_ViewModel.koSPA_Detail.Id());
 koSPAParm.koSPA_ViewModel.locationList();
 })
 };
 koSPAParm.koSPA_ViewModel.findDetailFromList = function (Id) {
 var detail = ko.utils.arrayFirst(this.koSPA_List(), function (t) {
 return Id === t.Id();
 }, this);
 return detail;
 };
 koSPAParm.koSPA_ViewModel.removeDetailFromList = function (Id) {
 this.koSPA_List.remove(function (item) {
 return item.Id() === Id;
 })
 };

//////////////////////////////////
 //Sammy
 //////////////////////////////////
 var SammyDipatcher = function (Method, Param1) {

switch (Method) {
 case "":
 case "List":
 koSPAParm.koSPA_ViewModel.koSPA_List_Message("");
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleList(true);
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail(false);
 break;
 case "DetailUpdate":
 koSPAParm.koSPA_ViewModel.koSPA_Detail_Message("");
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleList(false);
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail(true);
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail_Update(true);

var detailFromList = koSPAParm.koSPA_ViewModel.findDetailFromList(Param1);
 koSPAParm.koSPA_ViewModel.setListToDetail(detailFromList);

break;
 case "DetailCreate":
 //koSPAParm.koSPA_ViewModel.koSPA_Detail_Message("");

koSPAParm.koSPA_ViewModel.koSPAIsVisibleList(false);
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail(true);
 koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail_Update(false);

koSPAParm.koSPA_ViewModel.initDetail();

break;
 }
 }
 // Sammy を使ってブラウザの履歴に対応する(順番関係があるので、一番したにTOPを設置する。)
 this.SammyApp = $.sammy(function () {

//Method/Parm1
 this.get("#/:Method/:Param1", function () {
 var Method = this.params["Method"];
 var Param1 = parseInt(this.params["Param1"]);
 SammyDipatcher(Method, Param1);
 });

//Method
 this.get("#/:Method", function () {
 var Method = this.params["Method"];
 var Param1 = -1;
 SammyDipatcher(Method, Param1);
 });

//Topページ( location.hashが空だった時の処理)
 this.get("", function () {
//Ver 1.0.1 修正 同じサイト以外は移動する。
 var word = String(window.location);
 if (word.indexOf(koSPA.location) !== -1) {
 var Method = "";
 var Param1 = -1;
 SammyDipatcher(Method, Param1);
 }
 else {
 location.href = word;
 }

});
 });
 };
 //run
 koSPA.App.prototype = {
 run: function () {
 ko.applyBindings(this.koSPA_ViewModel);
 this.SammyApp.run();
 this.koSPA_ViewModel.loadDetails();

}
 };
 return koSPA;
 });
})(jQuery, window);

参考

// ]]>

VisualStudio2013 MVC5 with Twitterbootstrapにおいて、Knockout.js でTodos のサンプルを組み込んでみた。

前回

Backborn.jsのTodosアプリでAzure SQL(クラウド)+MVC5(VS2013)の組み込み。

今回

前回はBackborn.js(MVVMフレームワーク)+Todosアプリは、、今回はKnockout.jsにて作成してみます。

C#とかVBAとかに慣れている私にとっては、非常にとっつきやすく、とても簡単なのが印象的でした。

DOMの要素(button, input, だいたいなんでも) に data-bindすれば、 foreachや埋め込み変数など使えますし。

ko.observableをつかえば、データが変更したイベントを受け取りViewを更新することもできます。

以下はイメージです。

MVC5_VS2013_Knockout_Todos0010

Web Siteは以下です

http://vs-samples.azurewebsites.net/Home/Knockout_Todos

サンプルコードは以下のGitHubに保存しました。

https://github.com/snoro/VS2013_MVC_Sample/tree/master/Backborn_Todos_WebAPI

/Views/Home/index.cshtml

@{
    ViewBag.Title = "Knockout_Todos";
}

<h2>Todo: Knockout.js with Twitterbootsrap</h2>
<div class="container">
    <input type="text" placeholder="Enter Todo" value="" data-bind="value: todoInputValue" />
    <button data-bind="click:addTodo">Add</button>
    <table class="table table-hover">
        <thead>
            <tr>
                <th>Todo</th>
                <th>Delete</th>
            </tr>
        </thead>
        <tbody data-bind="foreach: todoList">

            <tr>
                <td data-bind="text: todoText"></td>
                <td><button class="btn btn-default" data-bind="click:$root.removeTodo">削除</button></td>
            </tr>
        </tbody>
    </table>
</div>

@section scripts{
    <script type="text/javascript" src="~/Scripts/knockout-3.1.0.js"></script>
    <script type="text/javascript" src="~/Scripts/test.js"></script>

/Scripts/test.js

$(function () {

    //Modelを定義
    function TodoModel(value) {
        var self = this;
        self.todoText = ko.observable(value);
    }

    //ViewModelを定義
    function TestappViewModel() {
        var self = this;

        self.todoList = ko.observableArray(); //todoの配列
        self.todoInputValue = ko.observable(''); //todo 入力値

        //Todo追加
        self.addTodo = function (obj, e) {
            if (!self.todoInputValue()) return;

            //配列に追加する。
            self.todoList.unshift(new TodoModel(self.todoInputValue()));
            //入力値を消去する。
            self.todoInputValue('');
        };

        //To削除
        self.removeTodo = function (obj, e) {
            self.todoList.remove(obj);
        };
    }
    //binding (View - VIewMode)
    ko.applyBindings(new TestappViewModel());
});

参考

knockoutのホームページ

Knockout.js 日本語ドキュメント

Knockout-Bootstrap

Helping you select an MV* framework

Hello world, with Knockout JS and ASP.NET MVC 4!

clickサンプル

さくさくアプリ開発

Visual Studio 2013 GitHubの使い方

はじめに

最近よく聞く「GitHub」でVisual Studio 2013では標準で「GitHub」に接続できちゃいます。

そもそも「GitHub」は、GitというLinuxを作った人が、開発したオープンソースのバージョン管理ソフトです。

それを無料・有料でホスティングしているのがGitHubです。

そもそもGitHubとは一体何か?
http://jp.techcrunch.com/2012/07/15/20120714what-exactly-is-github-anyway/

Microsoftの少し前の技術ですと「Visual Source Safe」や、今ですと「Team Foundation Server」
などを使用していたと思いますが。

公開してオープンソースで開発するとなるとGitHubが便利です。
※公開しないプライベートでもできます。

自分のソースを公開したり、いっしょに開発したりするのによさそうです。

1. GitHubにユーザを登録しておきます。

githubvs0010

2. GitHubにからのリポジトリを作成しておきます。

Tabの[Repositories]で、[New]をおします。

githubvs0020

3. 今回は[Test3]でリポジトリを作成します。

githubvs0030

4. Visual Studio で、なんでもいいので、新規プロジェクトを作成します。

今回は[MVC5]で作成しました。

githubvs0040

5. Visual Studio の設定

[ツール][オプション]

[ソース管理][プラグイン選択][現在のソース管理プラグイン]を[Microsoft Git Provider] に設定する。

githubvs0050

6. GitHubへの接続設定

githubvs0060

ユーザ名、、電子メールアドレス等設定する。

githubvs0070

7. ソリューションを選んで右クリックしてコミットしてローカルリポジトリにソースを格納する。

githubvs0080

PCのローカルのリポジトリにコミットされます。

githubvs0090

8. リモートのGitHubと同期するために、[同期されていないコミット]ボタンを押す

最初のときだけGitHubのリポジトリのURLをきいてくるので入力します。

今回は https://github.com/snoro/Test3 と入力後発行します。

githubvs0100

そしてGitHubをみるとアップロードされています。

githubvs0110

補足1 GitHubのリモートアドレスの変更

GitHubのURLの設定のところは、ローカルリポジトリを作成後、同期のときの、1回しか、表示されないので、

もしかしたら、コマンド等でGitHub(リモートリポジトリ)のURLを変更しないといけないかもしれないのです。

わたしは、わからないので、ローカルリポジトリを新規作成して、GitHub(リモートリポジトリ)のURLを変更しています。

githubvs0120

補足2 カレントのローカルリポジトリの変更

ソリューション作成時は、カレントリポジトリにプロジェクトを作ります。

また、カレントローカルリポジトリは消せませんので、削除する場合等には切り替える必要があります。

その場合、電源プラグみたいなアイコンをクリックしてカレントにしたいローカルリポジトリをダブルクリックします。

githubvs0130

補足3 GitHubとの同期時に重複エラーが表示され、うまく同期でくなくなったら

GitHub側でReadme 等修正するとなぜか重複等で、同期が失敗する場合があります。

コマンドたたけば治せるのでしょうけど、

GitHub側を正としていいなら、簡単なのでGitHubから、もう一度複製しちゃってもいいです。

githubvs0140

参考

Create, Connect, and Publish using Visual Studio with Git