2008年6月13日金曜日

フリーマーケット

何気に海外のドラマを見てる時にフリーマーケットのシーンがあって、テロップに「Flea Market」って書いてて初めてフリマの意味を知りました。

さて、DataContractJsonSerializer使ってますか? かつて何でもありだったJavaScriptSerializerの次に出てきた標準クラスなんだけど、これを使おうとするときは基本型指定ですよね。

で、ASP.NET MVC Preview3になってからJSONをレスポンスに返すためControllerに新たにJsonResult Json(...)が定義されてますよね。 で、これってobjectを引数に渡すんだけど、型指定しないってことじゃないですか。もしやと思って、ソース見てみたんです。 そしたら...。

#pragma warning disable 0618 JavaScriptSerializer serializer = new JavaScriptSerializer(); response.Write(serializer.Serialize(Data)); #pragma warning restore 0618

こんなコードになってた。強引っすね。 いいのかな~。自分も使いたいんだけど、使っていいのかな~。 だってね、LINQ to SQLのデータモデルクラスをそのままJSONで扱いたいけど、DataContractじゃないし、DataMemberついてもないからそのままだと使えなくて、でもそのためだけに別クラス作るのも馬鹿らしいじゃないっすか。みんなどうしてるんだろう。

そしたらさ、ガスリー君(本物なのかな?)が言うんですよ。 First thoughts on ASP.NET MVC Preview 3 | Aaron Lerch 「The JavaScriptSerializer class is actually undeprecated in .NET 3.5 SP1. 」 って(...どういう意味?)。 使っていいのか!?いいのかガスリー君!?

2008年6月3日火曜日

MVCContribでRESTful

CodeProject: RESTful routing in ASP.NET MVC. Free source code and programming help こんなに簡単に...。

SimplyRestfulRouteHandlerを使えばいいよ!ってことですね。 そもそもMVCContribってなんですか?ってなもんです。

なので、ちょっとソースをダウンロードして確認(現時点でのダウンロードが55っていうのが人気の無さを物語ってる気がしなくもなくもないけど気にしない!)。 かゆい所に手が届く系のライブラリって感じ?

RESTfulを単純にHTTP Methodだけで考えれば、リソースに対してGET/PUT/DELETE/POSTでアクションを決定するようにすればいいよね。ってことはリソース=Actionじゃなくて、リソース=Controller。 でも、WebでHTMLをViewにする場合、入力ページ(新規と編集)もGETだからリソースのGETとかぶる。あと、コレクションのGETも。 そうなると少し面倒な感じがしなくもないけど、RoutingでうまいことなんとかなるのがASP.NET MVCのいいところ。 URIの設計をしてて思ったのは、URIのリソースをそのままコントローラと一致させないで、以下に内包したコントローラとして頭を切り替えるかが結構キモなんですね。

会社と社員を内包関係で表現すると /Companies/{companyId}/Employees/{employeeId}

だけど、それぞれがコントローラになるんですよ~。 てことは、個別にしてもいいじゃない?

/Companies/{companyId} /Employees/{employeeId}

でも、これだと所属を表現できないからヤダってこともあるでしょう(会社と社員じゃないサンプルの方がよかったね...)。 デフォルトアクションがそれぞれIndexだとしたら、CompaniesController.Indexと EmployeesController.Indexがそれぞれのコレクションリソースで、個別のリソースを取得するためにもうひとつModelアクションを定義。で、RoutingでうまくModelアクションにルーティングしてあげればうまいこと出来るよね。

GET /Companies → 会社一覧取得 GET /Companies/New→ 会社新規フォーム POST /Companies → 会社新規登録 GET /Companies/123 → IDが123の会社取得 GET /Companies/123/Edit → IDが123の会社編集フォーム PUT /Companies/123 → IDが123の会社更新 DELETE /Companies/123 → IDが123の会社削除

こうしたいって時のコントローラでのアクション。 ※前回書いたRESTfulAttributeがある前提。

[RESTful(Post="Create")]
public ActionResult Index(int? id) {...}

[RESTful(Put="Update",Delete="Destroy")]
public ActionResult Model(int? id)
{
 if (id.HasValue)
  return Show(id);
 else
  return New();
}

public ActionResult Update(int id) {...}
public ActionResult Destroy(int id) {...}
public ActionResult Show(int id) {...}
public ActionResult New() {...}

これをうまくRoutingでさばきます。

routes.MapRoute( "Companies-Model", "Companies/{id}/{action}", new { controller="Companies", action="Model" }, new { id = @"[\d]+" } ); routes.MapRoute( "Companies", "Companies/{action}", new { controller="Companies", action="Index" }, new { action=@"[a-zA-Z]*" } );

こうやって、2つのRoutingでひとつのコントローラに対する、ルーティングを登録すればIDのありなしをちゃんと1つのコントローラで処理できるから、なんて素敵にRESTful!ってなりませんかね。 ※正規表現はよしなに。

ROAなURIを考えた場合{controller}/{action}/{id}だとアクションがリソースになるけど、{controller}/{id}/{action}でコントローラをリソースにしちゃいましょうっていう。

/Companies/123/Employees/456

とかのルーティング↓。

routes.MapRoute(
 "Employees-Model",
 "Companies/{companyId}/Employees/{id}/{action}",
 new { controller="Employees", action="Model" },
 new { companyId=@"[\d]+", id = @"[\d]+" }
); 

※コレクション部のルーティングも同じように。

イロイロ内包するならたくさん書くか、"/Employees"を"/{controller}"にしてあげる。 統一インターフェースでのアクセスは規約でアクションを決め打ちすることで最強になるでしょう。 全然MVCContribの話じゃないっすね...。

で、これを簡単にするのにMVCContrib。 Simply Restful Routing ここにあるように、アクション名を規約に通りにつければ、HTTP MethodとURIを見て、うまいことルーティングしてくれます。PUT/DELETEはブラウザから送れないので、そこはHidden で"_method"にメソッド名を入れてオーバーロードPOST。Railsっぽく。 どっちを使うかは気分次第で!

2008年5月29日木曜日

進化の過程をウキウキウォッチング

いや~、ずいぶん変わってしまいました。 Preview2からPreview3への移行をしてみようと作業してて思ったのが、簡単にできるようになったかもってところです。 もちのろんでASP.NET MVC Preview3ですよ! 分かりやすくRESTfulフィルター作ります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication4.p3
{
 public class RESTfulAttribute : ActionFilterAttribute
 {
   public string Post { get; set; }
   public string Put { get; set; }
   public string Delete { get; set; }

   public RESTfulAttribute() : this("", "", "") { }
   public RESTfulAttribute(string post, string put, string delete)
   {
     Post = post;
     Put = put;
     Delete = delete;
   }

   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
     string httpMethod = filterContext.HttpContext.Request.HttpMethod.ToLower();
  
httpMethod = (filterContext.HttpContext.Request.Form["_method"] ??
filterContext.HttpContext.Request.HttpMethod).ToLower();

     var actions = new Dictionary() {
       {"post",Post!="" ? Post : filterContext.ActionMethod.Name + "Post" },
       {"put",Put!="" ? Put : filterContext.ActionMethod.Name + "Put"},
       {"delete",Delete!="" ? Delete : filterContext.ActionMethod.Name + "Delete"}
     };

     if (actions.ContainsKey(httpMethod) && actions[httpMethod] != "")
     {
       filterContext.Cancel = true;
     
       var controller = filterContext.Controller as Controller;
       var actionInvoker = new ControllerActionInvoker(controller.ControllerContext);
       actionInvoker.InvokeAction(actions[httpMethod], new Dictionary());
     
       return;
     }

     base.OnActionExecuting(filterContext);
   }
 }
} 

はぁ、もうこの時点で違うんだね。 InvokeActionがControllerじゃなくて、ControllerActionInvokerクラスに移動になりました。 素直に呼び出せるからこれの方がいいね。 2個目のパラメータの意味が不明。MVCのソース見てもExecuteで↑みたいにnewしてる。 RESTful属性をつけたアクションの実行時にPOST/PUT/DELETE毎にアクションを振り分ける処理です。 属性のパラメータで名前を指定しなかった場合は、自動で呼び出しアクション名+HTTP MethodをInvoke対象のアクション名にしてます。

ホントはGETのときにNew(新規フォーム)/Edit(編集フォーム)/Show(表示だけ)を振り分けるのもID見たりしてフィルターでやった方がよりカッコよしかも。あと、どの表現(XHTML、JSON、XMLとか)を返すかとかも拡張子みたいな形で分けるとなお素敵さアップ。 続いてHomeControllerにアクションを追加。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication4.p3.Controllers
{
 public class HomeController : Controller
 {
   public ActionResult Index()
   {
     ViewData["Title"] = "Home Page";
     ViewData["Message"] = "Welcome to ASP.NET MVC!";

     return View();
   }

   public ActionResult About()
   {
     ViewData["Title"] = "About Page";

     return View();
   }

   [RESTful]
   public ActionResult Resource(int? id)
   {
     return Content(string.Format("GET!({0}) - {1}", Request.Form["value"], id), "text/html");
   }

   public ActionResult ResourcePost()
   {
     return Content(string.Format("POST!({0})", Request.Form["value"]), "text/html");
   }
 
   public ActionResult ResourcePut(int id)
   {
     return Content(string.Format("PUT!({0}) - {1}", Request.Form["value"], id), "text/html");
   }
 
   public ActionResult ResourceDelete(int id)
   {
     return Content(string.Format("DELETE!({0}) - {1}", Request.Form["value"], id), "text/html");
   }

   public ActionResult Item()
   {
     ViewContext vc = new ViewContext(ControllerContext, "dummy", "", null, null);
     var page = new ViewPage();
     page.Html = new HtmlHelper(vc, page);
     page.Url = new UrlHelper(vc);

     string partial_html = page.Html.RenderUserControl("~/Views/UserControls/Item.ascx");
     return Content(partial_html, "text/html");
   }
 }
} 

Resourceって言う名前のアクションを定義して、RESTful属性をくっつけました。 前 (Preview2)まで、単純にテキストを出力するのがなかったから、RenderTextなんてのをControllerにくっつけてたんだけど、新たにContentって言うメソッドで出力できるようになりましたね!ちなみに今回は使ってないけどJsonもあるよ!

最後のItemアクションはユーザーコントロール(ascx)の実行結果を出力するため(パーシャルっす)のサンプル。これは前とほぼ変わってないけど、全体的に引数の数が増えてる感じ?

最後にページ部分。Home/Index.aspxを書き換えてます。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication4.p3.Views.Home.Index" %>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
<h2><%= Html.Encode(ViewData["Message"]) %></h2>

<p>
<a href="<%= Url.Action("Resource") %>/1" class="restful">GET</a>
<a href="<%= Url.Action("Resource") %>" class="restful">POST</a>
<a href="<%= Url.Action("Resource") %>/1" class="restful">PUT</a>
<a href="<%= Url.Action("Resource") %>/1" class="restful">DELETE</a>
</p>

<p>
 <a href="javascript://" class="partial">どろんじょ</a>
 <div id="partial"></div>
</p>

<script type="text/javascript">
Event.observe(window, 'load', function(){
 var baseAction = '<%= Url.Action("Resource") %>';
 $$('a.restful').each(function(anchor){
   anchor.observe('click',function(e){
     var method = anchor.innerHTML;
     var index = anchor.href.indexOf(baseAction);
     var url = index >= 0 ? anchor.href.substring(index) : baseAction;

     new Ajax.Request(url,{
       method: method,
       parameters:'value=restful',
       onComplete:function(ajax){
         alert(ajax.responseText);
       }
     });
     Event.stop(e);
   });
 });

 $$('a.partial').first().observe('click',function(e){
   new Ajax.Request('<%= Url.Action("Item") %>',{
     onSuccess:function(ajax){
       partial.innerHTML = ajax.responseText;
     }
   });
 });

});
</script>
</asp:Content>

UserControls/Item.ascxの中身は何でもいいんだけど、とりあえず↓。

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Item.ascx.cs" Inherits="MvcApplication4.p3.Views.UserControls.Item" %>
<div>
 <ul>
   <li>マジで恋する5秒前</li>
   <li>ムゴ、ン色っぽい</li>
 </ul>
</div>

処理を簡単にするために、Site.Masterでprototype.jsを読み込むようにしてます。 実行すると、最初が↓。 img.aspx

PUTリンクをクリックすると↓。

img.aspx2

んで、どろんじょリンクをクリックすると↓。

img.aspx3

これで今日からRESTful!

2008年5月28日水曜日

Preview3!!!

やっと出た~!!

ASP.NET MVC Preview 3 Release - ScottGu's Blog ASP.NET - Release: ASP.NET MVC Preview 3 Source Download details: ASP.NET MVC Preview 3

Northwind使ったサンプルプロジェクトも出てるけど。 Updated Northwind Demo For ASP.NET MVC Preview 3 このままじゃなんかエラーで動かない!

と、コメントに書いてる人もいて、同じくアセンブリをいったん削除(System.Web.Abstractions,Mvc,Routhing)してから、Program Filesに入ったDLLを参照で追加すると動いた。 ソースも即行ダウンロードしたけど、どこがどれだけ変わったのやら。 とりあえず、Preview3 Readme.docを見たけど、もっとこう細かいところの一覧とかも欲しかったりする。わがままですね。

でも、HtmlヘルパーのDropDownList(ListBoxも)がMultiSelectListを渡すようになって、ちょっと良くなったのはいいけどRadioButtonListは相変わらずstring[]を返すんだね。結果に対するAggregateによる生成はどうなんでしょうね。いいんですかね。

あと、せっかくだから(最近ドはまり気味の)RESTfulなサンプルにしてほしかったりするな~。 URIは名詞で!統一インターフェースで! あと、個人的にかなりショッキングなのがViewDataのModelプロパティ...。同じ名前でプロパティを定義するようにしてたからっていうね。悲しいね。

ソース見てて思ったけど、あれだね、カスタムViewDataと、標準ViewDataの両方がViewにわたるんだね。標準ViewDataのModelプロパティにカスタムViewDataが入ってる(カスタムとか標準って言い方をしないよね、きっと)。この辺の変更でHtmlヘルパーのデフォルト値指定が楽になるみたい。 これは素敵かもしれない。TempDataは相変わらずSessionなのがちょっと残念。 CopyToは同じの作ってたから、最初から欲しかったので最高! あとは、いじりつつ確認するぞ~!! ※416からでもずいぶん違うのね...。

2008年5月20日火曜日

ひた隠しに

動くものを作ろうと思うと、やっぱり流行りのスタイルに乗っかりたい。そう思うのは浅はかなことでしょうか。で、ASP.NET MVCでRESTfulっぽく作るならやっぱり、HTTP MethodでActionを自動で振り分けたい。 そう思うのは、いたしかたなし。

画像を操作するResourceControllerがあったとしましょう。 画像を追加したいならResourceControllerにPhotoAddNewとかPhotoCreateなんていうActionを作りたくなるでしょう。でも待って!それじゃRESTfulじゃないよ!

複数形とか単数形は置いといて、とりあえず/Photoに対するアクセスでCRUDを処理したいよね! 特に興味無いですか?あ、そうですか。

まぁ、聞いて下さいよ。 以前のエントリーでRESTfulFilterなんてものを書いてみましたが、ぶっちゃけあれじゃ効率悪し。 送り側にActionFilterAttributeを書く方法だと、毎回リクレクションが必要ジャン? それなら、受け側でHTTP Method毎のAction名を指定するほうがよっぽど効率がいいってものですよ。 その辺踏まえて、以下のようなActionに。

/ResourceController [RESTfulFilter] public void Photo(int? id){...} public void PhotoPost(){...} public void PhotoPut(int id){...} public void PhotoDelete(int id){...} ※ActionResultじゃなくてvoidなのは気にしない。

で、 RESTfulFilterのExecutingでHTTP Method見て、現在のAction名の後ろに自動でInvokeActionする名前を作る(もちろん、属性のパラーメータでHTTP Method毎のAction名をセットできるようにした方が優しい)。 こんな風にしとけば、ActionFilterAttributeは1か所に指定するだけで済むので、効率よしって話です。 が、その他のActionがpublic(じゃないと、InvokeAction出来ない、よね?)なので、直接そのActionにもアクセスできちゃうのが何か気持ち悪し。出来て何が悪いってわけじゃ全然ないんだけども。なんとなく。直接のアクセスは拒否したいな、なんて思ったり思わなかったり。

そんなときはまたしてもActionFilterAttribute作成しましょう。 たとえば...RequireReactionAttributeとかって言う名前で。 RESTfulFilter でInvokeActionする直前に、Controllerのプロパティで例えばIsReaction = tureとかしておけば、その先でIsReactionがfalseなら実行をキャンセル(filterContext.Cancel=true)にしちゃう。そうしとけば直接アクセスしてほしくないActionに制限かけれたり(Controllerのbaseプロパティで分かる方法あるのかな)。 同じく、RequireAjaxAttributeとかって言うのも作っちゃって、XmlHtpRequest以外の要求は拒否(リクエストヘッダに何か入れとく)なんてことも、簡単(ヘッダ書き換えできちゃうから完璧なわけじゃないけど)。 prototype.jsなら勝手にヘッダーに入れてくれて楽ちんだし、DELETEやPUTのときには_methodにHTTP Method入れてPOSTにするから、そこ見て判定するようにしとくと、さらに楽ちん。

まだまだ工夫の余地ありだけど、HTTP Methodの自動InvokeActionだけでも、だいぶコード量削減出来てうれしい限りです。

2008年5月14日水曜日

Pagenation

なんか英語だとかっこよく見えるけど、普通にページ切り替えのこと。 LINQ使ってIQueryableなデータをページ切り替えできるようにするってばよ。

ASP.NET MVC: PagedList<T> : Rob Conery

ここのソースをそのまま流用してもよし。少しいじるもよし。自作するもよし。 結局はこういう形に落ち着くはず。 IPagedListとインターフェースを定義しておくのにも訳があり。

ASP.NET MVC - Pagination View User Control | Code-Inside Blog International

↑こういうページ切り替えコントロールを作成する時に、インターフェース渡しにしておくことで、型に依存しなくて済むから。IPagedListを受け取るようにコントロールを作っておけば、どんな型でも関係なく、ページに関する情報のみ取得できるでしょ。

上記、いずれもASP.NET MVCって書いてるけど、LINQだからMVCかどうかは関係ないと思われる。 ところで、ASP.NET MVCに限らず、AJAX的なページの部分更新を行う場合って、一般的な方法というのがあるんだろうか。 今はMVCで作ってるとはいえ、MVCに限った悩みとは思えず。 特に、リストでデータを持ってる場合とか。

<div id="container">
 <ul id="list">
   <li id="item1">Data1</li>
   <li id="item2">Data2</li>
   <li id="item3">Data3</li>
 </ul>
</div>

こんな感じのHTMLを持ってるとして、ulにliを1つ追加するような処理ってどうしましょうか、っていうね。 1.ページ全体更新。 2.ul全体(ulタグ込み)を取得して、divのinnerHTMLを書き換え。 3.追加対象のli(liタグ込み)を取得して、ulのinnerHTMLに書き足し。 4.追加対象のliの中身(liタグ無し)だけを取得して、document.createElement('li')のinnerHTMLに値を入れた後、ulにappendChild。 5.JSONか何かでデータだけを取得して、クライアントサイドですべての表示要素をcreateElement。

上からデータ量の多い順(3と4は誤差の範囲だけど)。 1はそもそも、ページ全体なんだからAjaxとか関係なし。 5はサーバーサイドでのビュー定義がまったくもって役に立たなくなりかねないし、JavaScriptコードが多すぎて面倒。Google Gearとかだとこの方法じゃないとつらいと思われるけど、今回は対象外としたい。 2・3・4のどれを選ぶのが妥当なんだろうか。 2の場合、ページの初回表示の時に使用するであろうコントロール(ASP.NETだとUserControlだし、Railsならpartialか)を、 Ajaxの結果としてResponse.Write。データ1個を返すだけだったりしても、リスト全体が対象になる分、データ量は増えるけど処理は簡単。 3と4の場合(ほとんど一緒)、リスト全体を表現するコントロールと、リストアイテムのコントロールに分けて作っておけば、ページ出力の場合と、部分出力の場合でも、それほどコード量に差が出てくるわけでもないんだけど、データ量は少なく済む。 んじゃ、常に3か4でいいのか?ってことになるんだけど、結局4だとcreatElementしなきゃいけなくて、コードがめんどっちくなる。3はというと、結構いいんだけど、サーバーサイドのコントロール階層が少し深くなって見通し悪しな気がしなくもなく。 でも結局は、2と3の組み合わせ(適材適所で!)になるんだろうとは思うけど。 今はずっとASP.NET MVCで、PostBack方式でのWebFormsを全然使ってないんだけど、サニタイズ気をつけてれば、こっちのほうが自分の思考にあってて作りやすい気がしてならない。 それもこれも結局は好みの問題なんだろね。

ハァ~。NHLプレーオフ生で観たい。デトロイト今年こそやってくれそうだし。

2008年5月6日火曜日

面白いPNGの利用法

nihilogic: Compression using Canvas and PNG-embedded data 面白い人もいるもんだな~。 JavaScriptファイルをPNGファイルにしちゃって、CanvasのgetImageDataを使って再度JavaScriptに復元。 画像ファイルはあまりにも砂嵐。スクリプトの文字コードをそのままRGBにセットして、画像を生成してるからね。なので、8ビット(0~255)グレースケールの画像になるし、1文字が1ピクセル。そりゃ砂嵐だぜ。 CanvasはIEじゃ使えないからFirefoxとOpera、あと最新のWebkitでしか、この方法は使えませんよ、って言う事なんですね。 スゴイな。こんな発想なかった。GZipでいいじゃね~か、と思う自分がちょっと恥ずかしいっす。 スクリプトから画像生成するのはPHPのコードになってたけど、そのままだと試しにくいので、C#で書きなおしてみた(そのままデス)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace MakeJSPng.Controllers
{
 public class HomeController : Controller
 {
   public void Index()
   {
     string js = this.ReadFromRequest("JSSource") + "";
     int hash = Math.Abs(js.GetHashCode());
     string fileName = string.Format("{0}.png", hash);
     string filePath = Request.MapPath("~/PNGs/") + fileName;

     if (js.Length > 0)
     {
       ViewData["JSSource"] = js;
       byte[] bytes = Encoding.UTF8.GetBytes(js);
       int width = (int)Math.Ceiling(Math.Sqrt((double)bytes.Length));
       int height = width;

       using (Bitmap bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb))
       {
         int pos = 0;
         for (int y = 0; y < height && pos < bytes.Length; y++)
         {
           for (int x = 0; x < width && pos < bytes.Length; x++)
           {
             int val = (int)bytes[pos++];
             bmp.SetPixel(x, y, Color.FromArgb(0, val, val, val));
           }
         }
         bmp.Save(filePath, ImageFormat.Png);
       }
     }

     ViewData["PngPath"] = "/PNGs/" + fileName;

     RenderView("Index");
   }

   public void About()
   {
     RenderView("About");
   }
 }
} 

ASP.NET MVC Preview2で動くよ。 Home/Index.aspxは↓。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MakeJSPng.Views.Home.Index" %>
<%@ Import Namespace="MakeJSPng.Controllers" %>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">
<script type="text/javascript" src="/Content/pngdata.js"></script>
<% using (Html.Form<HomeController>(c=>c.Index())) { %>

<p>スクリプト</p>
<%=Html.TextArea("JSSource", ViewData["JSSource"]) %>

<p>
<%=Html.SubmitButton("Make", "生成") %>
</p>

<p>画像</p>
<img src="<%= ViewData["PngPath"] %>" id="png" />

<p>復元</p>
<%=Html.TextArea("PNGSource", "") %>
<% } %>


<script type="text/javascript">

var path = '<%= ViewData["PngPath"] %>';
loadPNGData(path, function decodeCallback(data){
 var element = document.getElementById("PNGSource");
 element.value = data;
});

</script>
</asp:Content>

でも、IEじゃ動かないよ! 試しに、prototype.1.6.0.2.jsを圧縮した画像↓。 img.aspx

砂嵐。 本家はこれが30KBなんだけど、こっちは50KBなんでじゃ~!!画像サイズ少し違うし。PHPでのデフォルトエンコードがUTF8じゃないのかもしれないしね。 本家は8bppだけど、こっちは24bppなのが原因?それじゃしょうがないよね! ところでPHPのコードは良くわかんない(GDが良くわかんない?)けど、TrueColorを保存して自動で8ビットになるんでしょうか? ちょっとした息抜きってことで。

2008年5月2日金曜日

SSDS

SQL Server Data Servicesの略でSSDS。 SQL Server Data Services

クラウドコンピューティングっていうんですかね。 公開は全然先の話だからAmazon,Googleにはずいぶん遅れてしまうけど、SQL ServerをエンジンにLINQを使ったオンライン上のDB。CPUとストレージリソースいずれも雲の中。 SQL Server Data Services Overview

ここから概要のPDFがダウンロードできるっす。 Customer { SSDS account (1..N) { Authority (1..N) { Container (0..N) { Entity (0..N) こんな階層。 Entities are “flat scalar property bags.” と、書かれてるように基本Dictionary<T1,T2>の型をEntityに入れて自分でフィールドを管理。クエリー発行はLINQで。 Jeff's thoughts on Software Architecture, Large Scale Services and the Technical world at large : Interacting with SQL Server Data Services using SOAP ↑ここに基本的なサンプルコードあり。.NET Framework上での開発ならSOAPでやれば、超簡単にプロキシができるからね。

発表はMIX08のときだっけ? 忘れかけてた矢先、Private Beta Invitationって書かれたメールが来た。 使えるようになるのはずいぶん先の予定、ってことでほっといたのに。 とりあえず、ドキュメントをダウンロードして読んでみます...。 簡単なサンプルとか作ったり? ストアド書けるのかな~...。

あと、dotNetOpenIDScott Hanselman's Computer Zen - The Weekly Source Code 25 - OpenID Edition ASP.NET MVCでの実装サンプルもあったから動かしてみたら、もちろんだけどちゃんと動いてちょっと感動。しかもコードはチョビット。

public void Authenticate() { var openid = new OpenIdRelyingParty(); if (openid.Response == null) { // Stage 2: user submitting Identifier openid.CreateRequest(Request.Form["openid_identifier"]).RedirectToProvider(); } else { // Stage 3: OpenID Provider sending assertion response switch (openid.Response.Status) { case AuthenticationStatus.Authenticated: FormsAuthentication.RedirectFromLoginPage(openid.Response.ClaimedIdentifier, false); break; case AuthenticationStatus.Canceled: ViewData["Message"] = "Canceled at provider"; RenderView("Login"); break; case AuthenticationStatus.Failed: ViewData["Message"] = openid.Response.Exception.Message; RenderView("Login"); break; } } }

サンプルそのままでスイマセン。 認証はOpenIDでデータストレージはSSDSで、っていうのが自分でリソースを持たない方法としてなかなかいいかも。BLOB持てるなら画像とかのファイルもSSDSなんだけどどうなんだろ。

2008年4月25日金曜日

ViewUserControlが恋しい

ASP.NET MVCと相変わらず格闘中。 で、Ajax的な機能を組み込んでるところで、とうとうViewUserControlのみのレンダリングが必要になってきました。ハァ~。先延ばしにしてもどこかに答えが出てくるわけじゃないんだな~。人気なさそうだしな~。

次のバージョンでどのくらい実装に変化が出るのかドキドキしつつ、とりあえずはPreview 2のソースを追っかけてどうやろうか思案。ViewEngineを実装したところで、ViewUserControlのレンダリング結果が欲しいだけなんだから、あんまり意味ない(よね?)。

とうとう、あれか、ComponentControllerの出番か。とか思ってサンプル作ってみたけど、全然関係なさげ。 ComponentController and ActionResult - ASP.NET Forums こんなの見ると萎える...。

ViewUserControlとComponentControllerはコンテキストが違うっつーだけなんすかね。 コントロールは同一コンテキスト(パイプライン)の中で、同じViewEngineに対してレンダリングするけど、コンポーネントは別のViewEngineへの出力を取得して、それを現在のコンテキストにマージ。そんな感じ? まぁ、いいや。 で、どうすっかな~とMVCのソースを眺めてたんだけども、UserControlExtentionsのコードをオマージュすればいいってところにたどり着きました。 最低限以下のコードをコントローラに書けばOKっす!

      ViewContext vc = new ViewContext(ControllerContext, "dummy", "", null,null);
      var page = new ViewPage();
      page.Html = new HtmlHelper(vc);

      string partial_html = page.Html.RenderUserControl("仮想パス/コントローラ.ascx");
      Response.Write(partial_html);

Pageの実体を参照しないViewContextを作って、空のViewPageにHtmlHelperと一緒に割り当て。 コントロールの中でUrlHelper使うなら、それもセットしておきましょう。

      page.Url = new UrlHelper(vc); 

もちろん、RenderUserControlだから独自ViewDataも渡せるし、そこは普通に。 無駄にPageインスタンス作るのが納得いかないけど、オリジナルもその手法だからこれでよかろうさ。 これで、HTMLの部分更新もコントロールをそのまま使って実装できるから簡単になるよね!

2008年4月1日火曜日

ASP.NET MVCでRESTful

いつまで続くこのシリーズ。 いい加減しつこいっすね...。

前回まででAjaxなForm認証ができるようになったので、今度はRESTfulに挑戦! で、ここで気になったのがWCF。以前にWCF使ってRESTfulだなんだとやってみたのに、改めて同じようなことを実装しなおすのはどうなんですか、という想いがこみ上げてくる。

でも、気になったので仕方ない。気になったもの負け。 今回はHTTP Methodをどうやってアクションに結びつけるのがいいのか結構悩みました。 出力するのはJSON固定で行こうと思うので、ControllerのViewEngineを切り替えずにお気軽JSON出力の仕組みは前回のまま。

ControllerFactoryがどうのこうの?カスタムActionFilterAttribute?そもそもルーティングの問題? イロイロ考えた結果、すごく簡単に実装する方法がありました。 「OnActionExecutionオーバーライドでよくね?」 HandleUnknownActionとかも考えてみたものの、これが一番簡単っぽいな、と。

  1. ActionFilterAttributeから派生したRESTfulFilterAttributeを作成。
  2. Controllerから派生したRESTfulControllerを作成。
  3. 通常のControllerを作って、親クラスをRESTfulControllerに変更。
  4. CRUDリクエストを受け付けるActionを決めて、その他のActionにRESTfulFilter属性を指定。
処理の流れとしては↓こんな感じ。
普通にルーティング(controller/action/{id})される。 ↓ RESTfulControllerのOnActionExecutionで実行前に処理を横取り。 ↓ HTTP Methodを取得。 ↓ GET以外(この制限はなくてもいいんだけど)ならRESTfulFilterのついたメソッドをリフレクションで抽出。 ↓ HTTP Methodに対応するActionをInvokeAction。同時に自身の実行をキャンセル。
簡単な例としてstaticなList<T>に対するCRUDを実装することにしてみました。 まずはActionFilterAttribute。
using System.Web.Mvc;

namespace MvcApplication1.Filters
{
 public class RESTfulFilterAttribute:ActionFilterAttribute
 {
   public string AttachedAction { get; set; }
   public string HttpMethod { get; set; }

   public RESTfulFilterAttribute() : this("", "") { }
   public RESTfulFilterAttribute(string action, string method)
   {
     AttachedAction = action;
     HttpMethod = method;
   }
 }
} 

AttachedActionっていうパラメータに、受付元のアクション名を入れる。ホントはこれを単純にstringじゃなくてUriTemplateとかにしたほうがカッコいいんだろうな~。 で、HttpMethodにはGET以外の処理したいHTTP Methodを入れる。 次に、ベースとなるコントローラを作成。 今回は1つしかRESTfulなコントローラを作ってないけど、他にも作るようならここで作ったControllerを親クラスに指定する。

using System; using System.Web.Mvc; using System.Reflection; using MvcApplication1.Filters; namespace MvcApplication1.Controllers { public class RESTfulController : Controller { protected override void OnActionExecuting(FilterExecutingContext filterContext) { string httpMethod = filterContext.HttpContext.Request.HttpMethod.ToLower(); if (httpMethod != "get") { string actionName = filterContext.ActionMethod.Name; Type ctrl = this.GetType(); if (ctrl.GetMethod(methodname).GetCustomAttributes(typeof(RESTfulFilterAttribute), false).Length != 0) return; var attrs = from mi in ctrl.GetMethods() from attr in mi.GetCustomAttributes(typeof(RESTfulFilterAttribute), false) select new { ActionName = mi.Name, Action = (RESTfulFilterAttribute)attr }; foreach (var attr in attrs) { if (attr.Action.AttachedAction == actionName && attr.Action.HttpMethod.ToLowner() == httpMethod) { filterContext.Cancel = true; InvokeAction(attr.ActionName); } } } } } }

追記 すいません。上記コード間違えてましたね。対応付けするアクション名を比較する部分がさっくり抜けてました。HttpMethodしかチェックしてないコードでしたね。 直しついでに、LINQにしておきました(少しだけカッコの数を減らせます)。 で、上記Controllerを親クラスにした、Controllerを作成。 内容としては、staticで持ってるList<T>に対するCRUD。単純ですね。 イメージはBookmarkを登録したりする感じにしてみたので、名前はBookmarksにしてます。 ※全然入力内容をチェックとかしてないから、あんまり意味ない...。

using System.Collections.Generic; using System.Web.Mvc; using MvcApplication1.Models; using MvcApplication1.Filters; namespace MvcApplication1.Controllers { public class Api2Controller : RESTfulController { public static List bookmarks = new List(); BookmarksViewData viewData = new BookmarksViewData(); public void Bookmarks(int? id) { if (id == null) InvokeAction("GetAll"); else InvokeAction("Get"); } public void GetAll() { viewData.data = bookmarks.ToArray(); viewData.result = true; RenderView("Bookmarks", viewData); } public void Get(int id) { if (bookmarks.Count > id) { viewData.data = new Bookmark[] { bookmarks[id] }; viewData.result = true; } else viewData.result = false; RenderView("Bookmarks", viewData); } [RESTfulFilter(AttachedAction = "Bookmarks", HttpMethod = "POST")] public void AddNew() { Bookmark bm = new Bookmark(); bm.Title = this.ReadFromRequest("title"); bm.Url = this.ReadFromRequest("url"); bookmarks.Add(bm); viewData.result = true; RenderView("Bookmarks", viewData); } [RESTfulFilter(AttachedAction = "Bookmarks", HttpMethod = "PUT")] public void Update(int id) { if (bookmarks.Count > id) { bookmarks[id].Title = this.ReadFromRequest("title"); bookmarks[id].Url = this.ReadFromRequest("url"); viewData.result = true; } else viewData.result = false; RenderView("Bookmarks", viewData); } [RESTfulFilter(AttachedAction = "Bookmarks", HttpMethod = "DELETE")] public void Delete(int id) { if (bookmarks.Count > id) { bookmarks.RemoveAt(id); viewData.result = true; } else viewData.result = false; RenderView("Bookmarks", viewData); } } }

この中のBookmarksっていうActionがすべてのHTTP Methodを受け付けるイメージです。 なのでアクセスするアドレスは↓こんな感じ。 GETで一覧取得 Bookmarks/ GETで1つ取得 Bookmarks/{id} POSTで新規作成 Bookmarks/ PUTで更新 Bookmarks/{id} DELETEで削除 Bookmarks/{id}

なんで、GETだけ特別なんだ...。そもそも受け付け用のアクションは定義しないで、HandleUnknownActionで全部処理しちゃってもいいね。

でも、直接個別のActionも呼び出せちゃうのはご愛敬。制限つけるならつけるで。 Bookmarkクラスは単純。

using System.Runtime.Serialization;

namespace MvcApplication1.Models
{
 [DataContract]
 public class Bookmark
 {
   [DataMember]
   public string Title { get; set; }
   [DataMember]
   public string Url { get; set; }
 }

 [DataContract]
 public class BookmarksViewData
 {
   [DataMember]
   public Bookmark[] data { get; set; }
   [DataMember]
   public bool result { get; set; }
 }
}

ついでに、ViewDataも定義しておきます。 コードばっかり書いて説明が少ないのは、面倒だからデス! さて次に、クライアント側の機能ですが、前に作ったのとほとんど変化なし。 前回のAjax認証のページにがっつり追加(Views/Home/Index)です。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %>
<%@import Namespace="MvcApplication1.Controllers" %>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">
<form>
 ID:<%=Html.TextBox("id") %><br />
 Title:<%=Html.TextBox("title") %><br />
 Url:<%=Html.TextBox("url") %><br />
 <br />
 <h2>ASP.NET MVCで実装</h2>
 <input type="button" value="全取得" onclick="mvcCall('Bookmarks/','get')" />
 <input type="button" value="取得" onclick="mvcCall('Bookmarks/'+$F('id'),'get')" />
 <input type="button" value="追加" onclick="mvcCall('Bookmarks/','post')" />
 <input type="button" value="更新" onclick="mvcCall('Bookmarks/'+$F('id'),'put')" />
 <input type="button" value="削除" onclick="mvcCall('Bookmarks/'+$F('id'),'delete')" />
 <div id="mvcRes"></div>
</form>

<script type="text/javascript">
function mvcCall(action,method)
{
 $("mvcRes").innerHTML = "loading...";
 var params = $H({"title":$F("title"), "url":$F("url")}).toQueryString();
 new Ajax.Request("Api2/" + action,
 {
   method:method,
   postBody:params,
   requestHeaders: ["Content-type","application/x-www-form-urlencoded"],
   onComplete: function(ajax){
     $("mvcRes").innerHTML = ajax.responseText;
   }
 });
}
</script>
</asp:Content>

PUT/DELETE出来るようにprototype.jsの改造版使います。

img.aspx ↑こんな具合に動きます。 全取得とか追加とか削除とか。 ちなみにIDとして指定するのはListのインデックスなので0から始まる数字になります。あ、Ajaxの実行結果のViewを書いてなかったですね。

using System.Web.Mvc;

using System.Runtime.Serialization.Json;
using System.IO;
using System.Text;

using MvcApplication1.Models;
namespace MvcApplication1.Views.Api2
{
 public partial class Bookmarks : ViewPage
 {
   public string ToJSON(object obj)
   {
     DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
     using (MemoryStream ms = new MemoryStream())
     {
       serializer.WriteObject(ms, obj);
       return Encoding.Default.GetString(ms.ToArray());
     }
   }

   public override void RenderView(ViewContext viewContext)
   {
     viewContext.HttpContext.Response.StatusCode = 200;
     viewContext.HttpContext.Response.ContentType = "application/json";
     string json = ToJSON(viewContext.ViewData);
     viewContext.HttpContext.Response.Write(json);
   }
 }
}

戻りのContentTypeに"application/json"と書くのを忘れずに。で、内容は単純にViewDataをJSONにシリアライズして出力するだけのものです。 ここまでで、一番つまずいたのがクライアント側からサーバーにデータを送るときのContentTypeの指定部分です。最初これを入れてなかった (Ajax.RequestのrequestHeadersに何も指定なし)おかげでPUT時の更新が常にnullという事態に陥りました。

GET とDELETEではQueryStringのidしか見ないから問題なし。POSTのときは指定がなくてもデフォルトで"application/x- www-form-urlencoded"が入るけど、PUTはそもそも改造版だから指定しないと入らなくて、いくらやってもサーバーで取得できない罠。 MVCのソース追っかけても単純にラッピングしてるだけでRequestを書き換えてるようにも見えなかったから、何がどうなってるのかすごく悩みました。 結局、Request.ContentTypeを見てみたら空っぽだったのを発見して、無事動くようになりました。prototype.jsに頼りきってたので罰が当たった。

ここまで来たら、次はこれのWCF版も欲しくなる。ルーティングとかどうなるんだろう...。

とかいろいろ悩んだものの、特に気にすることなくControllersとかViewsじゃないフォルダをルートに作ってそこにsvcを入れてしまえばいいことに気がついた。あえてMVCにルーティングしてもらうことないもんね。結局中身は以前作ったのと同じなので省略(RequestとResponseはJSON)...。 クライアント側だけ少し違う。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %>
<%@import Namespace="MvcApplication1.Controllers" %>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">
<form>
 ID:<%=Html.TextBox("id") %><br />
 Title:<%=Html.TextBox("title") %><br />
 Url:<%=Html.TextBox("url") %><br />
 <br />
 <h2>WCFで実装</h2>
 <input type="button" value="全取得" onclick="wcfCall('Bookmarks.svc/','get')" />
 <input type="button" value="取得" onclick="wcfCall('Bookmarks.svc/'+$F('id'),'get')" />
 <input type="button" value="追加" onclick="wcfCall('Bookmarks.svc/','post')" />
 <input type="button" value="更新" onclick="wcfCall('Bookmarks.svc/'+$F('id'),'put')" />
 <input type="button" value="削除" onclick="wcfCall('Bookmarks.svc/'+$F('id'),'delete')" />
 <div id="wcfRes"></div>
</form>

<script type="text/javascript">
function wcfCall(action,method)
{
 $("wcfRes").innerHTML = "loading...";
 var params = method=="post"||method=="put" ? $H({title:$F("title"),url:$F("url")}).toJSON() : "{}";
 new Ajax.Request("/Wcf/" + action,
 {
   method:method,
   postBody:params,
   requestHeaders: ["Content-type","application/json"],
   onComplete : function(ajax){
     $("wcfRes").innerHTML = ajax.responseText;
   }
 });
}

</script>
</asp:Content>

ContentTypeとpostBodyの中身がJSONになってるのと、リクエスト先が違うだけ。 結局、ASP.NET MVCだけで作るのと、WCFも組み合わせて作るのとどっちがいいんだろう。書かなきゃいけないコード量はほとんど違わないし(RESTfulControllerとRESTfulFilterは最初に1回書くだけだし)。 Action単位のアクセス制限とか、今回試してないけどその辺で違いが出てくるのかな~。 Nikhil Kothari's Weblog : Ajax with the ASP.NET MVC Framework

そうそう、↑ここにAjaxでの部分更新(ascx単位)のやり方が書いてた。部分更新だけなら実装はシンプルで、ViewUserControlのRenderViewを自分で呼び出して返すようなものでした。だけど、その周りにあるいろんな実装が大量でゲンナリ。 調べるだけなのはもう疲れたので、せっかくだからASP.NET MVCでなんか作ってみようっと。

TempDataとRailsのflash

ASP.NET MVCをちょこちょこ調べるシリーズの最後に何か作ってみようと思って、ヘコヘコやってるところなんだけど、TempDataにやられた...。

もちろんベータ版だし、最終的には解決されるんだろうとは思うけど、少なくとも今のバージョンでは、SQLServerをセッションストアにしたときに、TempDataが機能しませぬ。 TempDataはそもそもSessionを使って実装してるって言うのは、まぁそうだろうと(どっかに書いてたし)。RedirectToActionとかした場合に、コンテキストが切り替わって情報を持っていけないんだから納得です。 でも、そのTempDataの実装、TempDataCollectionが内部で使ってるのが Pair<Dictionary<string, object>, HashSet<string>>。これがシリアライズできない...。だからInProcならいいけど、SQLだとSessionに入らないです。もちろんStateサーバーもシリアライズできないからダメ。

どうしたものか。SessionをInProcで使うのってなんか、気持ち悪いじゃないですか(そんなこと無い?)。 なので、もっと簡単に同じ仕組みを作ってしまえばいいんじゃないかと思って、FlashData(railsの同じ仕組みはflashという)のを作ってみました。無駄な作業とか言わないで!

public class FlashDataCollection { const string SESSIONKEY = "RevasFlashDataSessionKey"; Dictionary _flashData; HttpContextBase _httpContext; public FlashDataCollection(HttpContextBase httpContext) { if (httpContext != null) { this._httpContext = httpContext; this._flashData = httpContext.Session[SESSIONKEY] as Dictionary; this._httpContext.Session.Remove(SESSIONKEY); } if (this._flashData == null) this._flashData = new Dictionary(); } public object this[string key] { get { if (this._flashData.ContainsKey(key)) return this._flashData[key]; else return null; } set { this._flashData[key] = value; this._httpContext.Session[SESSIONKEY] = this._flashData; } } }

こんなクラスで。 やってることは↓これだけ。

  • ControllerのExecuteでFlashDataCollectionをnew。
  • FlashDataCollectionのコンストラクタで、Sessionに入ってるならそっちを復元してSessionからRemove。
  • 無ければDictionary<string,object>をnew。
  • 後は、インデクサで値をセットするたびにSessionに入れなおす。
んで、ControllerはExecuteをoverrideしたCusomControllerを作っておいて、こっちを派生させる。

public FlashDataCollection FlashData = null; protected override void Execute(ControllerContext controllerContext) { this.FlashData = new FlashDataCollection(controllerContext.HttpContext); base.Execute(controllerContext); }

こんな感じでいいんじゃないかと。 Controller ではTempDataの代わりにFlashDataに値を入れて、RedirectToActionしたときとかにはこっちを参照。ただし、これだけだと、Viewで参照できない(Controllerに入れちゃってるんで)ので、今度はRenderViewをoverrideするかとも思ったけど、そこまでしなくても次のバージョンとかではTempDataもシリアライズできるようにしてくると(期待して)思うので、今回はあえて CustomViewDataを作って、そのViewDataクラスに値を入れ込んでいくって言う方法にしときました(分かりにくい説明だ...)。 たとえば、Home/Inputっていうアクションで入力した内容をHome/Saveって言うアクションにPOSTしたとき、入力値の検証なんかは ModelでやるからSaveで値を取得した後にエラーだと判明するわけだったりするじゃないですか。そしたらエラーメッセージと入力値を TempDataに入れて、Home/InputにRedirectToActionで飛ばすでしょ。そんな使い方のときにうまいことやりたいっす。 ViewDataはSerializable属性付けて自分で作ったクラスを使うようにしておけば、うまくいくっちゃ! そんなこんなでなかなか先に進まないっちゃ! ところで、RouteValueDictionaryのコンストラクタってちょっと不思議だな~、と思ってどうやってんのかと思ったら、普通にリフレクションしてるだけみたい。

routes.Add(new Route("{controller}/{action}/{id}", new MvcRouteHandler()) { Defaults = new RouteValueDictionary(new { action = "Index", id = "" }), });

↑この無名クラスをnewで初期化してるとこ。:action=>"Index",:id=""みたいな書き方で、Dictionaryになるんだよ?

public static System.Collections.IDictionary MakeDictionary(object withProperties) { System.Collections.IDictionary dic = new Dictionary(); foreach (var property in withProperties.GetType().GetProperties(BindingFlags.Public|BindingFlags.Instance)) { dic.Add(property.Name, property.GetValue(withProperties,null)); } return dic; }

↑こんな感じの実装でちゃんとDictionaryになるのね。シンプルでよろしだね。

Turn Anonymous Types into IDictionary of values - ISerializable - Roy Osherove's Blog

2008年3月21日金曜日

ASP.NET MVCでAjaxな認証

ASP.NET Ajaxとの相性が良くないよね、という思いからprototype.jsへ返り咲き。 jQueryにはすごいのがあったしね。あ、でもそこまで(Controllerを派生させてExecuteとRenderViewをオーバーライドして、Site.Masterか Ajax.Masterか切り替え。さらにクライアントサイドのjQueryで対象エレメント部だけを抽出しなおして描画に反映。すごくて感動した)のはちょっと面倒なので、軽くね。

  1. まず、普通にASP.NET MVC(Preview2)でプロジェクト作成。
  2. そしたらControllers/Models/Views/Contentフォルダができるので、このContentにprototype.jsを入れる。
  3. Controllersに名前を"SecureController"を作成(なんでもいいんだけど)。
  4. Controllersに名前を"ApiController"を作成(なんでもいいんだけど)。
  5. Views/Homeフォルダに"Mvc View Content Page"をテンプレートに"Login.aspx"を作成。
  6. 同じくViews/Apiフォルダ(作る)に"Mvc View Page"(ここ大事!)をテンプレートに"Login.aspx"を作成。
  7. Views/Secureフォルダ(作る)に"Mvc View Content Page"として"Index.aspx"を作成。

ここまでで準備完了。イメージ的には/Secure/Indexはログインしてないとアクセスできないよ、って言う感じです。 ルートのweb.configでForm認証使うことと、セキュアなパスを指定。

        <authentication mode="Forms">
           <forms loginUrl="/Home/Login"></forms>
       </authentication>

↑これと↓これ。

<location path="Secure"> <system.web> <authorization> <deny users="?"/> </authorization> </system.web> </location>

Views/Shared/Site.Masterのhead要素内にprototype.jsへのscriptタグを作成。

    <script src="/Content/prototype.js" type="text/javascript"></script> 

※常にルートを指すようにしてるけど、相対で参照するようにしたい場合は、ヘルパーを作りましょう。標準で用意してくれることを希望しつつ。 Views/Home/Indexの中身は単純にSecure/Indexへのリンクだけ。これをActionLink<T>で作成(なんかこれがお勧めなリンクの作り方っぽい)。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %>
<%@import Namespace="MvcApplication1.Controllers" %>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">
   <h2>Ajaxな認証機能</h2>
   <p>
   <%= Html.ActionLink<SecureController>(c=>c.Index(),"Secure Page") %>
   </p>
</asp:Content> 

※@importでコントローラのnamespaceを指定しておく。

img.aspx

"Secure Page"のリンク先は"/Secure/Index"(Indexは省略されてるけど)。 中身は単純に↓こんな感じにしときました。

<h2>要認証!!</h2>
ログインしてないとみれないよ。<br />
Welcome <b><%=User.Identity.Name %></b>!!

この段階でリンクをクリックすると認証してないので、単純に”Home/Login”にリダイレクトされます。なので、今度はそっちを用意。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="MvcApplication1.Views.Home.Login" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">
<form>
 <table>
 <tr>
   <th>Longin ID</th>
   <td><%=Html.TextBox("id") %></td>
 </tr>
 <tr>
   <th>Password</th>
   <td><%=Html.Password("pass") %></td>
 </tr>
 <tr>
   <td colspan="2"><input type="button" value="Login" onclick="login()" /></td>
 </tr>
 </table>
</form>
<script type="text/javascript">
function login() {
 var params = $H({"id":$F("id"), "pass":$F("pass")}).toQueryString();

 new Ajax.Request("/Api/Login",{
   method:"post",
   postBody:params,
   onComplete:function(req){
     var status = req.status;
   
     //req.responseText
     if (status==200)
       alert("Success!! " + req.responseText);
     else
       alert("Fail... " + req.responseText);
   }
 });
}
</script>
</asp:Content> 

認証するためのリクエストパスが"Api/Login"になってるのと、認証結果はRESTfulっぽくステータスコードで判定。今回は成功すれば200(OK)、失敗すれば406(Not Acceptable)を利用(使い方があってるのか自信ないけど...)。 HTTPステータスコード – Wikipedia

続いて、Api/Loginの中身。今回は単純なIDとPasswordを利用してみます。実際にはMembershipを使うと思うけど。

    public void Login()
   {
     string id = this.ReadFromRequest("id");
     string pass = this.ReadFromRequest("pass");

     ViewData["login"] = false;
     if (id == "yama" && pass == "kawa")
     {
       ViewData["login"] = true;
       FormsAuthentication.SetAuthCookie(id, false);
     }

     RenderView("Login");
   } 

続いて、Views/Api/Login。 ここは戻り値を返すだけの機能にしたいし、レスポンス内容をJsonにしてみるために、Masterを参照しないページ(Mvc View Page)にしました。

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="MvcApplication1.Views.Api.Login" %>
(<%= ResponseJson() %>) 

ページの中身は↑これだけ。

public void Page_Load() { Response.ContentType = "application/json"; if ((bool)ViewData["login"] == true) Response.StatusCode = 200; else Response.StatusCode = 406; } public string ResponseJson() { return "{\"result\":" + (ViewData["login"]+"").ToLower() + "}"; }

↑コードもこれだけ。Jsonをベタ書きしてるけど、実際には上手いことシリアライズを使うでしょうね。 で、動かす! img.aspx2

あえて、違うID/Passwordを入れてみる。

img.aspx3

正しく入れる。

img.aspx4

この成功か失敗の判定はレスポンス内容のJsonをevalして判定せず、XHRのstatsプロパティが200かどうかで判定。RESTfulっぽい~。 画像だと分かりにくいけど、画面のリフレッシュは発生してないからね! 最後に、Home/Indexに戻って"Secure Page"のリンクをクリックすると~。

img.aspx5

イエ~イ。 実際にはControllerでSetAuthCookieやるより、Viewでやるの?ViewでJsonとか返したかったらControllerFactory使うの?えらい人教えて! これやってて思ったけど、承認はアクション毎にかけるのがMVCっぽいんだね(今回は違うけど)。リソースに対するアクションに権限があるかどうかの方が、コントローラに権限があるかどうかよりもRESTfulだし。フォルダに制限をかけるっていう発想じゃなくて、誰がどのリソースにどんな権限があるかってことだもんね。 結局ActionFilterAttributeで制限かけるのが王道になるのかな~。

2008年3月20日木曜日

ASP.NET MVCでアクセス制限をかける

Webアプリケーションには必ず必要だよね、認証(Authentication)と承認(Authorization)。 はてさてMVC形式でフォルダが割り振られ、物理フォルダの階層とルーティング機能でアクセスするURLのマッピングが一致してないASP.NET MVCではどのようにアクセス制限をかけましょうか。悩ましいですたい。

とりあえずスコットさんとこで確認したところMembershipやらRoleやらは今まで通り使えるってことなので、その辺aspnet_regsqlコマンドとか使って事前に準備を済ませ、ユーザー登録部分も普通にCreateUserWizardコントロールで作成、ログインはLoginコントロールを使って作成。 ただし、この場合runat="server"属性のformが必要になるので、美しさは少し欠ける。綺麗にするならHtml.Formヘルパー使って form要素を作成し、Viewもちゃんと自分でHTML書いてやるのがいいですよね。でも、その方法だと試しに作るには面倒すぎるので、とりあえずでよければ既存コントロールを使って自身にPostbackした後、Response.RedirectでContoroller/Action形式なりの URLに飛ばすことで認証はOKでしょう。

で、今度は承認。 まずはオーソドックスにViewsフォルダの中のController別フォルダにweb.configを入れてみた。 が、しかし上手くいかず。Authorizeするタイミングがこのタイミングだと遅いんだろうか。 ※本家Scottさんとこのコメント欄でそれらしいこと書いてた(ルーティングはOnPostMapRequestRequestHandler eventの後ですよって)。 で、ってことで、ルートのWeb.Configにlocation入れてみた。そしたら、これはすんなりできた。ロールに合わせてコントローラを分けるように作ればフォルダ単位で制御できるから今までとおなじように扱える。ただし、pathにアクションまで含めても上手くいかなかった。そんな使い方するのかどうか微妙だけど、気になるから調べてみたら、2つの方法で解決できる模様(実質1つか)。 まずは標準の方法でPrincipalPermission属性をアクション(コントローラのメソッド)に指定する方法。 ASP.NET MVC framework - Security もうひとつがSystem.Web.Mvc.ActionFilterAttributeを派生させてカスタム属性を作ってそこで制御する方法(要するにどっちも属性なんだけど)。 Rob Conery » ASP.NET MVC: Securing Your Controller Actions どっちも例外発生するからApplication_Errorで例外をハンドリング。 アクション毎に制限をかけたいっていう需要がどの程度これから出てくるか(個人的にそういう設計にするかどうか)分からないけど、ソースをいじるより web.configいじったほうが気分が楽だから、コントローラ単位の承認(Authorize)がいいんじゃなかろうかと思う次第でやんす。 全然話はかわるんですが↓これがちょっとすごい。 Chris van de Steeg’s blog - What’s practical is logical ASP.NET MVC+jQuery。テンプレートまで用意してる。ViewでJSON返すよ~とか、Ajaxで簡単取得(Htmlヘルパーも拡張してる)とか。

2008年3月13日木曜日

ASP.NET MVCとASP.NET AJAX

ポストバック手法とMVC手法は相互に乗り入れるということを前提に作るものじゃないってことなんだろうか。 ASP.NET MVCでのサンプルを作ってみて、コントローラ/アクションからビューへの展開が今までのポストバック手法(WebForms)に比べて新鮮で面白い。クリーンなHTML(ViewStateとかその他のスクリプト)を出力するし。 型指定のViewDataじゃないと無駄にコードが長くなる(Viewの中で)ので、基本的にはきちんとViewData用のクラスを用意して利用(ViewPage<ViewDataの型>)するべし。

    <tr>
     <th>カテゴリ</th>
     <td><%=Html.Select("CategoryID", ViewData["Categories"] as List<Category>, (ViewData["Product"] as Product).CategoryID)%></td>
   </tr>
   <tr>
     <th>業者</th>
     <td><%=Html.Select("SupplierID", ViewData["Suppliers"] as List<Supplier>, (ViewData["Product"] as Product).SupplierID)%></td>
   </tr>
   <tr>
     <th>単価</th>
     <td><%=Html.TextBox("UnitPrice", (ViewData["Product"] as Product).UnitPrice)%></td>
   </tr>

↑キャスト長すぎ...。

  public class ProductEditViewData
 {
   public Product Product { get; set; }
   public List<Supplier> Suppliers { get; set; }
   public List<Category> Categories { get; set; }
 }

↑これがあれば↓これで済む。

    <tr>
     <th>カテゴリ</th>
     <td><%=Html.Select("CategoryID", ViewData.Categories, ViewData.Product.CategoryID)%></td>
   </tr>
   <tr>
     <th>業者</th>
     <td><%=Html.Select("SupplierID", ViewData.Suppliers, ViewData.Product.SupplierID)%></td>
   </tr>
   <tr>
     <th>単価</th>
     <td><%=Html.TextBox("UnitPrice", ViewData.Product.UnitPrice)%></td>
   </tr> 

※ベタだけどNorthwind使ってスコットさんと同じように作ってみました。 ちなみにこれらのFormの値をコントローラで取得する場合は、UpdateFormを使う模様。

Product product = new Product(); BindingHelperExtensions.UpdateFrom(product, Request.Form, new[] { "ProductName", "CategoryID", "SupplierID", "UnitPrice" });

※ヘルパーとか12月のやつからちょこちょこ変わってる。 Formの値を個別に取得する場合は、コントローラの拡張メソッドになってるReadFromRequestを使う。 Html の要素作成にヘルパー使うなら気にしないけど、手書き(input/radio/textarea/select/hidden)のときはnameに指定したものじゃないとサーバーで値取れません(単なるRequest.Form/QueryStringだから当たり前なんだけど)。 runat="server"じゃないし。 で、もちろんViewStateなんてないのでイロイロこまごましたのは自分でちゃんと実装する必要があり。Pageのインスタンスをサーバーサイドで再構築出来ないんだから。 ここで気になるのがASP.NET AJAXとの絡み。試してみたけどやっぱりすんなりできるものじゃないっす。ASP.NET AJAXのうまみは基本的に全く利用できない感じです。UpdatePanelとかに必要なデータを自分で文字列生成して返すとか考えられない。自身にポストバックする従来の方法なら、いくらでもPage再構築出来るかもしれないけど、コントローラに"post"するんだからサーバーサイドでPageの再構築とか厳しい。 そうなると、普通に自分でAjaxを実装(もちろんライブラリを使うけど)していくことになって、あっちをとればこっちが立たずみたいな。機能的に完全に分離してもよくて、運用サイドが使うような機能なら従来の手法(楽ちんに作れる)、ユーザーサイドが使うならMVC(綺麗に作れる)とかなのかな~?

そもそもASP.NET AJAXを使ってないから、あんまり気にしないんだけど、実際市場ではどういう使い分けをするのかが気になるデス。 そうそう、ASP.NET MVCのパフォーマンスってどうよ?みたいなことも、もちろん気になるわけで。

従来のフローをはしょって書くと↓。 System.Web.HttpRuntime → System.Web.HttpApplication → Pageのインスタンス

MVCだと↓。

System.Web.HttpRuntime → System.Web.HttpApplication → System.Web.Mvc.MvcHandler(ルーティング) → System.Web.Mvc.Controller(ターゲットアクション探す:リフレクション) → System.Web.Mvc.ActionFilterExecutor(アクション起動) → 自分のコントローラ → System.Web.Mvc.WebFormViewEngine(ビューへの橋渡し) → System.Web.Mvc.ViewPage.RenderView(ビュー準備) → Pageのインスタンス → 自分のView

たぶんこうなんじゃないかと...。 なんとなくそうなのかなっていう程度です。 途中"自分のコントローラ"の直前に[ネイティブからマネージの移行][マネージからネイティブへの移行]っていうのが入ってて、これが何してんのかよくわかなかった。 ※ちなみに、IISじゃなくてVS2008から起動するWebServer上で、デバッグ実行時の呼び出し履歴を見ただけなので、かなり推測でしゃべってます。 少なくとも、Page(View)に到達するまでに結構いろいろやってる。でもポストバック元のPageをオンメモリに再構築するっていう手間がないから、結局どっちがいいのか判断難しいところです(CPUリソースかメモリリソースかっていう問題なのかな)。 あとね~、LINQ to SQL。DataContextは結局コントローラのプロパティにして使いまわすのでいいんじゃないかな。いろいろなところで、 HttpContext.Current.Itemsに突っ込んで同一コンテキスト内ではシングルトンな使い方がいいみたいに書いてたけど。

public class DContext { public static T Get() where T : System.Data.Linq.DataContext, new() { // HttpContextが不明なら常にnewして返す if (HttpContext.Current == null) { System.Diagnostics.Debug.WriteLine("no context!"); return new T(); } T context; // ItemのKeyを生成(同一コンテキスト内なら同じキーができるはず) string key = "__WRSCDC_" + HttpContext.Current.GetHashCode().ToString("x") + Thread.CurrentContext.ContextID.ToString(); context = HttpContext.Current.Items[key] as T; if (context == null) { context = new T(); HttpContext.Current.Items[key] = context; System.Diagnostics.Debug.WriteLine("new context!! key:" + key); } return context; } }

※試しに作ったクラス。

パフォーマンスをテストしてみたけど、普通にコントローラでDataContext使うのが早かった。 もちろんコード自体は分離していかないと、ごっちゃごちゃになっちゃうけど、DataContextそのものをPartialで書き足して行かないで、別途機能ドメイン毎にクラスを作って、それぞれの中で個別にDataContextのインスタンスを作るくらいでいいんじゃないかと。 複数のDataContextが1度のリクエストで発生するのが無駄遣いなら、コントローラで作ったDataContextのインスタンスを渡すとか。

なんだかんだ大量のデータ更新を一度にやりたい時とかはストアド使ってやればいいし。ホントはバッチ更新とかできるのかなとも思ったけどそれは無理みたい。

Public Sector Developer Weblog : I was wrong :(

トランザクションが良くわかんなかったけど、分散でまたがないし、複数DataContextを使わないならTransactionScopeいらないっぽい。SubmitChangesが上手いことやってくれるっぽい。

方法 : データ送信をトランザクションで囲む (LINQ to SQL)

ちなみに↑これはts.comlete()を実行してないから動かないと思うんだけど、どうなんでしょう(試した限りではコミットしてくれなかった)? なんだかんだと、まとまりのない文章になっちゃった。 誰かえらい人イロイロ教えて。

2008年3月12日水曜日

Normalization

ASP.NET MVC p2をいじってみてるんだけど、クラスのリファレンスが見当たらないのは気のせいなのかな。RouteValueDictionaryって?IDictionary<string,object>のコンストラクタの使い方が分からなくて足止め。 ASP.NET MVC Normalization - ASP.NET Forums

new Dictionary<string,object> { {"class", "user-login" } } 
↓instead of  
new { @class = "user-login" }  

@classってなんだ!?Verbatim?ダブルクォーテーションなくてもいいのか。結局はリテラルと同じ動きになるね(匿名クラスでclassって名前のメンバを使うからあえて@つけてるのかな~)。 匿名クラスはそのままConsole.WriteLineで書きだすと、inspect的な出力になるのに、class定義して同じようにしてもクラス名しか出力しない。c#でinspectってないのかな。

class cdct { public string key1 { get; set; } public string key2 { get; set; } public string key3 { get; set; } } var dct1 = new { key1 = "value1", key2 = "value2", key3 = "value3" }; var dct2 = new cdct { key1 = "value1", key2 = "value2", key3 = "value3" }; Console.WriteLine(dct1); // => { key1 = value1, key2 = value2, key3 = value3 } Console.WriteLine(dct2); // => ConsoleApplication.cdct

簡単な拡張メソッドでそれっぽくしてみた。

static class ObjectExt { public static string Inspect(this object obj) { return Inspect(obj, false); } public static string Inspect(this object obj, bool withType) { bool first = true; string inspect = ""; Type t = obj.GetType(); foreach (MemberInfo mi in t.GetMembers()) { if (mi.MemberType == MemberTypes.Property) { inspect += (first ? "" : ",") + mi.Name + "=" + t.GetProperty(mi.Name).GetValue(obj, null); first = false; } } return (withType ? t.ToString() : "") + "{" + inspect + "}"; } }

へぼいから取扱注意。

Console.WriteLine(dct1.Inspect()); // => {key1=value1,key2=value2,key3=value3} Console.WriteLine(dct2.Inspect()); // => {key1=value1,key2=value2,key3=value3}

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。