だるろぐ

明日できることは、今日しない。

WebMatrix 2:RESTful?な Web アプリケーション (5)

WebMatrix 2:RESTful?な Web アプリケーション (4) - だるろぐ の続き。結局こんな感じになったというのを書くのを忘れていた。実際に動作するサンプルは、Windows Azure に置いておいた。

モーダルダイアログの表示に pointer-events というのを使ったのだけど、実はこれ、IE/Opera では使えないらしい。興味のある人は Mozilla Firefox か Google Chrome で試してほしいかな。デザインの過程は WebMatrix でほかの Web サイトのデザインをパク……じゃなくて、参考にさせていただく - だるろぐ を参照。

データベースとのやり取り

~/App_Code/Post.cs に書いておいた。一つのファイルにごちゃっと書いておけるのが ASP.NET Web Pages のお手軽なところだと思うけれど、まぁ、わけておいた方があとあとメンテナンスしやすそう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.WebPages;
using WebMatrix.Data;

public class Post
{
    // ここら辺はテーブルの構造をそのまま定義
    public long Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }

    public string Excerpt // 本文の一部だけを取得
    {
        get
        {
            return Body.IsEmpty()
                ? string.Empty
                : string.Join("", Body.Take(40));
        }
    }

    // クエリで受け取ったデータ(dynamic)から
    // Post オブジェクトを生成
    public Post(dynamic record)
    {
        Id = record.Id;
        Title = record.Title;
        Body = record.Body;
        CreatedAt = record.CreatedAt;
        UpdatedAt = record.UpdatedAt;
    }
}

public static class PostRepository
{
    public static IEnumerable<Post> List()
    {
        const string query = "SELECT * FROM Post ORDER BY CreatedAt DESC";

        using (var db = Database.Open("db"))
        {
            return db.Query(query).Select(_ => new Post(_));
        }
    }

    public static Post Find(long id)
    {
        const string query = "SELECT * FROM Post WHERE Id=@0";

        using (var db = Database.Open("db"))
        {
            return new Post(db.QuerySingle(query, id));
        }
    }

    // タイトルでも検索できるように
    public static Post Find(string title)
    {
        const string query = "SELECT * FROM Post WHERE Title=@0";

        using (var db = Database.Open("db"))
        {
            return new Post(db.QuerySingle(query, title));
        }
    }

    public static long Create(string title, string body)
    {
        const string query = "INSERT INTO Post (Title, Body, CreatedAt, UpdatedAt)"
                           + "VALUES(@0, @1, @2, @3)";
        var now = DateTime.Now;

        using (var db = Database.Open("db"))
        {
            db.Query(query, title, body, now, now);
            // INSERT した列の Id を取得するのってこうやればいいらしい
            var result = db.QueryValue("SELECT @@IDENTITY");
            return Convert.ToInt64(result);
        }
    }

    public static void Update(long id, string title, string body)
    {
        var query = "UPDATE Post SET Title=@0, Body=@1, UpdatedAt=@2 WHERE Id=@3";
        var now = DateTime.Now;

        using (var db = Database.Open("db"))
        {
            db.Query(query, title, body, now, id);
        }
    }

    public static void Delete(long id)
    {
        var query = "DELETE FROM Post WHERE Id=@0";

        using (var db = Database.Open("db"))
        {
            db.Query(query, id);
        }
    }
}

これで List()、Find()、Create()、Update()、Delete() が使えるようになった。

メソッドに応じて異なるビューを呼び出す

~/Posts.cshtml の中身。IIS はデフォルトで PUT/DELETE が使えないみたい。Web.config を書き換えてもいいのだけど、ここは _method = "PUT" がリクエストに含まれていたら PUT として処理することにした。

@using System.Reflection

@try
{
    // POST で PUT/DELETE を代用
    string method = Request.HttpMethod.ToUpper();
    if (IsPost && Request["_method"] != null) 
    {
        method = Request["_method"].ToUpper(); // Camelize() とか作っておくといいのかも
    }

    // パラメーターの型を解釈(なるべく long に、無理なものは string のまま)
    var args = UrlData.Select<string, object>(_ =>
    {
        try { return long.Parse(_); } catch { return _; }
    }).ToArray();

    // メソッドの検索
    MethodInfo method_info = this.GetType().GetMethod(
        method,
        args.Select(_ => _.GetType()).ToArray()
    );

    // 検索したメソッドを呼び出してビューをレンダリング
    @method_info.Invoke(this, args);
}
catch (Exception e)
{
    throw new ApplicationException();
}

@functions // HelperResult を返せば、@... で呼び出してビューがレンダリングされる
{
    private const string VIEW_POST_DETAIL = "~/Views/Posts/_ShowPost.cshtml";
    private const string VIEW_POST_ARCHIVE = "~/Views/Posts/_ListPosts.cshtml";
        
    public HelperResult GET() // Camelize() すれば Get() で呼び出せるケド
    {
        var posts = PostRepository.List();
        
        switch (Request.ContentType) // JSON でも返せるように!
        {
            case "application/json":
                RenderJason(posts);
                return null;
            default:
                return RenderPage(VIEW_POST_ARCHIVE, posts);
        }
    }

    public HelperResult GET(long id)
    {   
        var post = PostRepository.Find(id);
        CheckIfPostIsNull(post);
        
        switch (Request.ContentType)
        {
            case "application/json":
                return RenderJason(post);
            default:
                return RenderPage(VIEW_POST_DETAIL, post);
        }
    }

    public HelperResult GET(string title)
    {
        var post = PostRepository.Find(title);
        CheckIfPostIsNull(post);
                
        switch (Request.ContentType)
        {
            case "application/json":
                return RenderJason(post);
            default:
                return RenderPage(VIEW_POST_DETAIL, post);
        }
    }

    public void POST()
    {
        var title = Request["Title"];
        var body = Request["Body"];
        var id = PostRepository.Create(title, body);

        switch (Request.ContentType)
        {
            case "application/json":
            default:
                RenderJason(id);
                break;
        }
    }

    public void PUT(long id)
    {
        var title = Request["Title"];
        var body = Request["Body"];
        PostRepository.Update(id, title, body);

        switch (Request.ContentType)
        {
            case "application/json":
            default:
                RenderJason(id);
                break;
        }
    }

    public void PUT(string title)
    {
        var post = PostRepository.Find(title);
        CheckIfPostIsNull(post);
        
        var new_title = Request["Title"];
        var body = Request["Body"];
        PostRepository.Update(post.Id, new_title, body);

        switch (Request.ContentType)
        {
            case "application/json":
            default:
                RenderJason(post.Id);
                break;
        }
    }

    public void DELETE(long id)
    {
        PostRepository.Delete(id);

        switch (Request.ContentType)
        {
            case "application/json":
            default:
                RenderJason(id);
                break;
        }
    }

    public void DELETE(string title)
    {
        var post = PostRepository.Find(title);
        CheckIfPostIsNull(post);
        
        PostRepository.Delete(post.Id);

        switch (Request.ContentType)
        {
            case "application/json":
            default:
                RenderJason(post.Id);
                break;
        }
    }
}

@functions
{
    private HelperResult RenderJason(long id) // Id だけを JSON で返す
    {
        Response.Clear();
        Response.ContentType = "application/json";
        Response.Write(string.Format(@"{{ ""Id"": ""{0}"" }}", id));
        Response.End();
        return null; // null を返すなら HelperResult 型にしなくてもいい気はする
    }
    
    private HelperResult RenderJason(object post) // Post だの Posts だのを JSON で返す
    {
        Response.Clear();
        Response.ContentType = "application/json";
        Response.Write(Json.Encode(post));
        Response.End();
        return null;
    }
    
    private void CheckIfPostIsNull(Post post)
    {
        if (post == null)
            throw new HttpException(404, "Post is not found.");
    }
}

f:id:daruyanagi:20130124072621p:plainf:id:daruyanagi:20130124072619p:plain

Content-Type によって応答が切り替わるようにしてみた。

ビュー

全部のっけても仕方ないので、~/Views/Posts/_ShowPost.cshtml" だけを。

@{
    var model = PageData.First().Value as Post; // もはやイディオムかな……
    Page.Title = model.Title;
    Page.PostId = model.Id;
    Layout = "~/_SiteLayout.cshtml";
}

<h1>@Page.Title</h1>

@model.Body

@section Dialog // 編集作業はモーダルダイアログで処理することにした
{
    <aside id="EditDocument" class="modal">
        <script type="text/javascript">
        $(function () {
            $('#edit-document').click(function () {
                $.ajax({
                    url: "@Request.Url.AbsolutePath",
                    type: 'POST',
                    data: {
                        '_method': "PUT",
                        'Title': $('#Edit-Title').val(),
                        'Body': $('#Edit-Body').val(),
                    },
                    success: function (data) {
                        var uri = '@Request.Url.GetRoot()/Posts/' + data.Id;
                        window.location.replace(uri); // リダイレクト
                    },
                    error: function (data) {
                        alert('failure');
                    },
                });
            })
        });
        </script>
	    <div class="modal-wrapper">
            <header class="modal-header">
                <h1><input type="text" id="Edit-Title" value="@model.Title" /></h1>
            </header>
            <div class="modal-content">
                <textarea id="Edit-Body" placeholder="なんか書いてもええんやでぇ">@model.Body</textarea>
            </div>
		    <footer class="modal-footer">
                <a href="javascript: void(0)" id="edit-document" class="button">OK</a>
                <a href="#close" title="Close" class="button">キャンセル</a>
            </footer>
	    </div>
    </aside>

    <aside id="RemoveDocument" class="modal">
        <script type="text/javascript">
        $(function () {
            $('#remove-document').click( function () {
                $.ajax({
                    url: "@Request.Url.AbsolutePath",
                    type: 'POST',
                    data: {
                        '_method': "DELETE",
                    },
                    success: function (data) {
                        var uri = '@Request.Url.GetRoot()/Posts/';
                        window.location.replace(uri);
                    },
                    error: function (data) {
                        alert('failure');
                    },
                });
            })
        });
        </script>
	    <div class="modal-wrapper">
            <header class="modal-header">
		        <h1><span id="Delete-Title" >@model.Title</span> を削除しようとしています……</h1>
            </header>
            <div class="modal-content">
                <p>本当に削除しますか?</p>
            </div>
		    <footer class="modal-footer">
                <a href="javascript: void(0)" id="remove-document" class="button">OK</a>
                <a href="#close" title="Close" class="button">キャンセル</a>
            </footer>
	    </div>
    </aside>
}

普通の MVC フレームワークだと

  • GET /Posts/Edit/:Id

なんかで編集画面に遷移すると思うのだけど、今回は

  • GET /Posts/:Id#Edit

で編集ダイアログを表示するようにしてる(今は開発中の可読性のために #EditDocument としているけど)。ルーティング処理を手抜きしたかったのもあるし、こうした方がかっこよくないか? ってところもあって。そんなわけで、リダイレクト処理もサーバー側が処理するのではなくて、ビューの JavaScript が担当するようになっている。

積み残した部分もあるけど、とりあえずはこれで終わり。つぎは予定通り Twitter/Facebook 認証を使ってみようかと思う。