technoshop

杉並区和泉のソフトウェアハウス、株式会社テラソフトの技術ブログです。主に.NET MVCとKnockoutJSの情報をまとめます。

.Net MVC4 で KnockoutJS ~【基礎編3】成績表を作ってみよう(Bootstrapでデザインも!)Part1

ザクです。台風18号が来たため午前中は家で仕事してました。

基礎編3回めです。Knockoutの練習に使えるエクセルっぽいものって何かなと考えていました。データが学校関連なので、成績表にしてみました。学生さんには、一肌脱いでもらうことになります。

Bootstrapでテーブルを作る

今の見た目があまりのもダメなので、ここらでBootstrapの出番です。クラス登録日(Enrollment Date)とかどうでもいいですし、MVC4でCRUDもやらないので余計な部分を別のに書き換えます。

<h2>Student Grades</h2>

<table class="table table-bordered">
    <tr>
        <th rowspan="2">
            Last Name
        </th>
        <th rowspan="2">
            First Name
        </th>
        <th rowspan="2">
            Credits
        </th>
        <th colspan="3">
            Grade
        </th>
    </tr>
    <tr>
        <th>
            A
        </th>
        <th>
            B
        </th>
        <th>
            C
        </th>
    </tr>
    <!-- ko foreach: Students -->
    <tr>
        <td><span data-bind="text: LastName"></span></td>
        <td><span data-bind="text: FirstMidName"></span></td>
        <td>38</td>
        <td>1</td>
        <td>3</td>
        <td>1</td>
    </tr>
    <!-- /ko -->
</table>

Index.cshtmlのスクリプト部分はそのままで、HTMLの部分だけ上のように書き換えてみました。tableタグにBootstrapのclassを入れました。

f:id:technoshop:20141003112845j:plain

みちがえるように成績表っぽくなりました!それぞれの生徒さんが今何単位(Credits)取れていて、成績(Grade)ABCがそれぞれ何個あるのか集計するアプリを作ります。エクセルちっくですね。単位は成績がとれたクラスの単位を集計し、個々の成績を数えます。アメリカの大学卒業生の成績確認によく使うGPA(Grade Point Average)なんかも横に出せそうです。

MVC4のモデルでWeb APIで取得するデータを調整する

ビューのひな形ができたので、ここに入れるデータを確認します。EnrollmentとCourseのデータとか見てみます。Advanced Rest ClientでEnrollmentデータを出してみると、GradeはABCの文字列ではなくIntで出てますね。C#Enum使ってるので仕方ありません。0がA、1がB、2がCなので、Knockout側で適当に変換して使います。単位のデータはCourseの方にありますね。Enrollmentで持ってるIdから引っ張ってこれそうです。MVC4側を調整して、Courseのデータも取ってこれるようにしてみましょうか。

Web APIで何のデータを取ってくるかはモデルのファイルに[JsonIgnore]をつけたり外したりするんでしたよね。まずEnrollmentを直します。

    public class Enrollment
    {
        public int EnrollmentId { get; set; }
        public int CourseId { get; set; }
        public int StudentId { get; set; }
        public Grade? Grade { get; set; }

        public virtual Course Course { get; set; }
        [JsonIgnore]
        public virtual Student Student { get; set; }
    }

Courseのナビゲーションプロパティの上にあった[JsonIgnore]を消しました。これでEnrollmentの子(Studnetの孫)データとして、Courseのデータが取れるはずです。次にCourseです。

  public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseId { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        [Display(Name = "Department")]
        public int DepartmentId { get; set; }

        [JsonIgnore]
        public virtual Department Department { get; set; }
        [JsonIgnore]
        public virtual ICollection<Enrollment> Enrollments { get; set; }
        [JsonIgnore]
        public virtual ICollection<Instructor> Instructors { get; set; }
    }

綺麗に階層化されたJsonデータを作ろうと思うと、こちらではむしろ親テーブルその他へのリファレンスが邪魔になるので[JsonIgnore]をナビゲーションプロパティにつけまくります。とりあえず、これ以上のデータは今のところ不要なので全部に付けました。もう一度、Advanced Rest Clientでデータを見ます。

f:id:technoshop:20141003133011j:plain

これなら使えそうです。

KnockoutのビューモデルをWeb APIのデータに合わせる

成績表のデータの準備はできました。次は表示用にKnockoutのビューモデルを直してやります。


    // Enrollment Model
    function Enrollment(enroll) {
        var self = this;

        self.EnrollmentId = ko.observable(enroll.EnrollmentId);
        self.CourseId = ko.observable(enroll.CourseId);
        self.StudentId = ko.observable(enroll.StudentId);
        self.Grade = ko.observable(enroll.Grade);
    self.Title = ko.observable(enroll.Course.Title);
        self.Credits = ko.observable(enroll.Course.Credits);
    }

    // Student Model
    function Student(student) {
        var self = this;

        self.StudentId = ko.observable(student.StudentId);
        self.LastName = ko.observable(student.LastName);
        self.FirstMidName = ko.observable(student.FirstMidName);
        self.EnrollmentDate = ko.observable(student.EnrollmentDate);

        self.Enrollments = ko.observableArray().ofType(Enrollment);
        self.Enrollments(student.Enrollments);
    }
    function SchoolViewModel() {

SchoolViewModelの上の部分をこのように直してみました。Studnetモデルの中で、self.Enrollmentsをko.observableArrayで作り、ビューモデルからみたStudent同様にEnrollmentのモデルを上に定義しました。中身はほぼMVC4のモデルの中身と同じです。違うのは、Courseモデルが持っているデータもEnrollment側に持たせた部分です。これはこの後でやる単位の合計を計算するのに都合がいいからこうしました。Web API経由でKnockoutのデータをSave(PUTを使う)したりする場合は、これではモデルがMVC4と合わないので使えなかったりするのですが、まだSaveはしませんし、Courseのデータを変えることはなさそうなので、しばらくこのままで行きます。

Courseからのデータの取り方だけざっくり説明しておくと、ここで受け取ったJsonデータ(enrollの中身)は、JavaScriptのチェインの仕組みを使って取ることができます。enrollの中にはCourseのオブジェクトがぶら下がっているのでそれを書いて、Courseオブジェクト内の変数名を指定するだけで値を取ってこれます。(KnockoutのObservableではないのでおしりに括弧は要りません)

これでビューモデルに必要なデータが取れるか見てみます。ブラウザでStudentをリロードしてください。それで、Knockout Context Debuggerで適当な生徒の上で右クリックします。

f:id:technoshop:20141003143501j:plain

生徒Alexanderが受講している3つの授業のオブジェクトが配列として取れていることが確認できました。TitleやCreditsの値もちゃんと入っています。

ko.computedのなかでko.utils.arrayForEachを使いまくる!

やっと、このチュートリアルのテーマ「Knockoutでエクセルちっく」の本題に入ってきました。KnockoutにはJavaScriptの中でObservableArrayを便利に扱うユーティリティがいくつか用意されています。しかし本家のサイトでこの解説がないんですよね、なぜか。代わりにKnockout情報満載な「Knock Me Out」というブログでの解説ページへのリンク貼っておきます。


Utility functions in KnockoutJS - Knock Me Out

こういう所がKnockoutなんだかなぁ~という気がしますが、他の人がちゃんと説明しているだけマシですか。サイトで説明されているko.utils.arrayForEachを使います。これはC#のForEachとほぼ同じことをやります。さっそく生徒さんの取得単位の合計を計算するko.computedを作ってみましょう。

    // Student Model
    function Student(student) {
        var self = this;

        self.StudentId = ko.observable(student.StudentId);
        self.LastName = ko.observable(student.LastName);
        self.FirstMidName = ko.observable(student.FirstMidName);
        self.EnrollmentDate = ko.observable(student.EnrollmentDate);

        self.Enrollments = ko.observableArray().ofType(Enrollment);
        self.Enrollments(student.Enrollments);

        // Calculate Total Credts for what a student passes
        self.totalcredits = ko.computed(function () {
            var total = 0;
            ko.utils.arrayForEach(self.Enrollments(), function (el) {
                var value = el.Credits();
                if (!isNaN(value)) {
                    total += value;
                }
            });
            return total;
        });
    }

生徒が取っている授業のデータをグルグル回して単位の合計を計算するにはStudentモデルの中でやるのが良さそうです。(その生徒の取得単位ですもんね)Observableを定義している下にself.totalcreditsをko.computedで作りました。

中身はごく単純に、Enrollmentの配列をko.utils.arrayForEachで回して、Creditsの値をtotal変数に積算しています。最後にこれをreturnして出力します。もう一度ブラウザをリロードして、デバッガの結果を見てみます。

f:id:technoshop:20141003150227j:plain

アレックス君の取得単位「9」がEnrollmentの配列のデータを使ってちゃんと計算されました。他の生徒も大丈夫そうです。ビューに表示できそうです。

ひとまず中間成績発表

ビューモデルに取得単位のデータが準備できました。これをビューにバインドして表示します。単にテキストとして表示するならtextで良さそうです。

    <!-- ko foreach: Students -->
    <tr>
        <td><span data-bind="text: LastName"></span></td>
        <td><span data-bind="text: FirstMidName"></span></td>
        <td><span data-bind="text: totalcredits"></span></td>
        <td>1</td>
        <td>3</td>
        <td>1</td>
    </tr>
    <!-- /ko -->

HTMLのテーブルで、Studentsのforeachをやっている部分にtotalcreditsを入れました。ブラウザで表示してみます。

f:id:technoshop:20141003151155j:plain

ちょっwww 全体的に成績はよくないです。2名やばい生徒がいますね。長くなったので続きはPart2で。