2009年6月13日土曜日

全然意識したこと無かったけど

ASP.NET MVCだからどうのこうのっていう話ではないんだけど、ちょっと衝撃。

ASP.NET MVC Routing vs. Reserved Filenames in Windows - Stack Overflow

↑ここで初めて目にした時は"へぇ~"くらいだった。

bitquabit - Zombie Operating Systems and ASP.NET MVC

↑ここで2回目に目にした時は"マジやべ~"に変わった。デフォルトルート設定のままCom1Controllerを作ってアクセスしてみた。

ng1

ぬふ。404ですね。普通に開発してたら、Com1Controllerなんて作ったりしないから平気だろう、なんて甘いこと考えてたらダメですよ!

続いて、HomeControllerのIndexアクションを以下のようにしてみましょう。普通です。

    public ActionResult Index(string id)
    {
      ViewData["id"] = id;

      return View();
    }

で、Home/Index.aspxにこれまた以下のようなコードを書いたとしましょう。

  <p>
  id="<%= Html.Encode(ViewData["id"]) %>"
  </p>
  
  <div></div>
  <dl>
    <dt>ダメなパターン</dt>
    <dd>
    <%
      foreach (var id in new[]{"COM","LPT"}.SelectMany(
              l=>Enumerable.Range(1, 9).Select(r=>l+r))
                           .Union(new[]{"CON","AUX","PRN","NUL"}))
        Response.Write(Html.ActionLink(id, "Index", new { id }) + " ");
    %>
    </dd>
    <dt>いいパターン</dt>
    <dd>
    <%
      foreach (var id in new[] { "COM0", "COM1029", "id", "PRINTER", "NULL" })
        Response.Write(Html.ActionLink(id, "Index", new { id }) + " ");
    %>
    </dd>
  </dl>

ワクワクしますな。ドキドキが止まりませんな。

ng2

こんなのが表示されますね。もうあからさまに怪しいのが見てとれます。早速リンクをクリックしてみようじゃないですか。

まずは、下段の"COM0"。普通上段からだよ...。まぁ、いいでしょう。

ng3

ちゃんと出ます。続いて"COM1209"、"NULL"も試してみたけどちゃんと出ます。

今度は上段の"COM1"。

ng4

ぎゃふーん!!

でも、変な値が出るわけじゃないから、まぁいいか。そんなURL入れるのが悪い!なんて思ってたら変な障害出たりしてあたふたすることになりかねないですよ!だって、IDに文字列入力許すような設計って普通じゃないですか。んで、ルーティングのパラメータにID含むようなURL設計って当たり前すぎるじゃないですか。でも、ID入力時に"COM1"やら"NUL"やら"AUX"やらはじくような処理を入れるのが当たり前なわけじゃないじゃないですか。当たり前なんですか?そーだったらすいません。

何となくRouteCollection.RouteExistingFiles プロパティ (System.Web.Routing)でtrueにしてみたけど、ダメだったから、そういうもんだとして入力値のチェックをちゃんとやろうと思うところです。

2009年6月7日日曜日

Json.NETとStringTemplateでお気楽HTML出力

暇だったんですよね。で、暇つぶしにJSONからStringTemplateを通してHTMLをはき出させてみたんです。何となくですけど、適当にデータを定義しておいて、テンプレートに当てはめてHTMLを出力するっていうツールがないものかと探してみたんだけど、どーにも楽ちんそうなのが見あたらなくて。あ、ちなみに5月の話です。

データはXMLでも良かったんだけど、書くのが面倒になるのもやだし、テンプレート解釈とかは作りたくないし、ってことで、Json.NET - James Newton-KingStringTemplate Template Engineで書いてみたっす。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Antlr.StringTemplate;
using System.IO;
using Newtonsoft.Json.Linq;

namespace STHtml
{
  public class STGenerator
  {
    private const string INPUT_PATH = "input";
    private const string ROOT_PROPERTY = "templateItems";
    private const string FILENAME_PROPERTY = "output_path";
    private const string FILENAME_FORMAT = "output{0}.txt";

    private STSetting _setting;

    public STGenerator(STSetting setting)
    {
      _setting = setting;
    }

    public void Execute(IOutput output)
    {
      var group = new StringTemplateGroup("htmlTemplates", INPUT_PATH);
      var json = LoadJSON();

      if (json == null)
        return;

      // 
      var list = (from item in json[ROOT_PROPERTY]
                  select item).ToList();

      foreach (var item in list)
      {
        // テンプレートの読み込み
        var st = group.GetInstanceOf(_setting.TemplateFileName);

        // ファイル名は?
        var filePath = GetOutputFilePath(item, list.IndexOf(item));

        // 生成
        var generated = GenerateFromJSON(st, item);
        if (generated == null)
          return;

        // 出力
        output.Print(filePath, generated);
        Console.WriteLine("output -> " + filePath);

      }
      Console.WriteLine("{0} file(s) template generated.", list.Count());
    }

    private string GetOutputFilePath(JToken item, int itemIndex)
    {
      // ファイル名は?
      var fileName = (from t in item
                      select item[FILENAME_PROPERTY]).FirstOrDefault();
      var filePath = Path.Combine(_setting.OutputPath,
        fileName != null ?
          fileName.ToString().Replace("\"", "") :
          string.Format(FILENAME_FORMAT, itemIndex)
      );

      return filePath;
    }

    private JToken LoadJSON()
    {
      string jsonText;
      JObject json = null;

      try
      {

        jsonText = File.ReadAllText(Path.Combine(INPUT_PATH, _setting.JsonFileName));
        json = JObject.Parse(jsonText);
      }
      catch (Exception e)
      {
        Console.WriteLine("データファイルの書式が間違ってるか、ファイルがないか...");
        Console.WriteLine(e.Message);
        json = null;
      }

      return json;
    }

    private string GenerateFromJSON(StringTemplate st, JToken json)
    {
      try
      {
        // JsonからDictionaryに変換
        // JObjectからそのままはStringTemplateが無理さ
        var dict = JsonToDictionary(json) as Dictionary<string, object>;
        foreach (var kv in dict)
        {
          st.SetAttribute(kv.Key, kv.Value);
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("変換に失敗したよ。テンプレートがおかしいと思われる。");
        Console.WriteLine(e.Message);
      }

      return st.ToString();
    }

    private object JsonToDictionary(JToken token)
    {
      Dictionary<string, object> result = new Dictionary<string, object>();

      // なんか美しくないね。
      // 値セットと値戻しが同列だしな~。まぁ、いっか。
      foreach (var node in token)
      {
        if (node is JProperty)
        {
          // プロパティ型(name:value)なら取得
          var prop = node as JProperty;
          // 配列([...])かオブジェクト({...})なら再帰
          if (prop.Value.Type == JsonTokenType.Array ||
              prop.Value.Type == JsonTokenType.Object)
          {
            result[prop.Name] = JsonToDictionary(prop);
          }
          else
          {
            // その他の値型なら文字列化
            var value = prop.Value.ToString()
                                  .Replace("\"", "");

            result[prop.Name] = value;
          }
        }
        else if (node is JArray)
        {
          // 配列型なら戻す(再帰の時にしか処理しないもん)
          var arr = node as JArray;
          var list = new List<object>();
          foreach (var item in arr)
          {
            list.Add(JsonToDictionary(item as JToken));
          }
          return list;
        }
      }

      return result;
    }
  }
}

説明するのが面倒なんでソースです。こんな適当な感じでもそれなりに動くよ~。JsonからHTMLへの埋め込み時にEncodeかかってないからその辺は気をつけましょう。

使い方なんですけど(使う人がいるとは思えないけど)。

まずはinputフォルダのdata.jsonファイルにデータを書き込んでいきます。例えば↓こんな感じです。

{templateItems:
  [
    {
      output_path:'test1.html',
      title:'ページ1',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'2.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'3.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    },
    {
      output_path:'test2.html',
      title:'ページ2',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    },
    {
      output_path:'test3.html',
      title:'ページ3',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'2.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    }

  ]
}

全体が一つのObjectです。で、ルートではtemplateItemsっていう名前の配列を定義がルールです。templateItems配列に入れる各オブジェクト(アイテムオブジェクト)の書式は気をつけて統一する必要あり。こんな時にJSONスキーマが役立つんだろうな~。面倒なので気をつけるっていうルールで。アイテムオブジェクトはどんな形式でもOK、のはず。最初に提示したソースのJsonToDictionary関数が再帰でその辺上手くやってくれるようにしてます。でも、JArray型とJProperty型以外は想定してないので、まぁ、その辺は雰囲気で。

あ、アイテムオブジェクトに絶対に入れなきゃ行けないのが出力ファイル名をセットしたoutput_path。上記の例だとoutputフォルダにそのまま出す感じなんですが、例えば"output_path:’folder1/default.html’"とかってしておくと、outputフォルダ内にfolder1フォルダを作成して、その中にdefault.htmlを出力します。

次にテンプレートファイルとして↓こんなのを用意。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <meta name="keywords" content="">
  <title>$title$</title>
</head>
<body>
<h1>$title$</h1>
<div>
  <h2>$subject$</h2>
  <ul>
    $gallery:{g|
    <li><img src="$g.src$" alt="$g.alt$" title="$g.title$" /></li>
    }$
  </ul>
</div>
</body>
</html>

後は実行するだけ。そうすると↓こんなのが出力されました。

sthtml1 sthtml2

またしてもプロジェクトファイルは添付しておくのでご自由に。

ズルズル滑る

いや~、負けちゃったね~。ボロッカスっすね~。そんな時もあるよね~。今期3回目のオグさんゴーリーもお疲れちゃんでしたね。内容的にはそんな悪くないと思うよ。

あれじゃない?今日は来れなかったけどコレでイマイちゃんも来たら女子6人だもんね。スゴイね。男子陣がんばんないとな~。個人的に前回もそうだったけどウィールがひどい。今日は雨で湿度が高かったっていうのもあるかもしれないけど、あまりにもひどい。たぶんね、傍目には単に「たけはらも衰えたな」と見えるかもしれないけど、もうね、ひどいよウィール。雨の戸塚みたいな感じよ。誇張抜きで。止まれない曲がれないダッシュ効かない。ひどいモンでした。前回よりもズルズルでよ~。参ったね。上半身は反応しても下半身が滑ってついてこないもどかしさ。あまりにもひどいから試合後新しいウィール買いました。次は普通に滑れるっす。セト君ゴーリーには期待出来ないから、とりあえずヤマちゃんには何もさせないようにちゃんと押さえ込むね。うしし。楽しみ~。

全然話は変わるんだけどね、最近「リファクタリング・ウェットウェア」いう本を読みまして。これがさ~、驚きの面白さ。最初の章で"ドレイファスモデル"っていう話が書いてあって、よく上手く言葉でここまで表現できるな~、とちょっと感動。自分で自分がドレイファスモデルで今どの段階だと思ってるかはちょっとナイショ。てへ。その後も脳がどういうふうに反応するとか、コンテキストが大事とか、二次無能力なんていう定義がちゃんとあったりするんだ~、みたいなとかね。

あとね~、世代間の"4種の原型"も。各世代での世界の見方が繰り返すっていうのとか。アンディーハントも最初のほうに書いてるけど、開発者が読む本じゃなくて誰が読んでも面白く読めると思う。ドレイファスモデルで特定のレベルを超えてる人なら特に。

絶対読んで損しないから読んでみたらいいと思うけど、6章の最初に引用してるマークトウェインの引用「ある経験から得た知識をすべてだと思い込み、先に進むのを躊躇すべきではない。熱いストーブの蓋に座った猫は、同じように熱いストーブの蓋には二度と座らない。それはよいのだが、冷たい蓋にもけっして座ろうとしなくなるだろう。」が上手にテーマを表現できてる気がするな~。ちなみに同じく6章で紹介されてるインナーゲームも読んだことがあって、その時ハンマーで殴られたような衝撃を受けたのを思い出した。

2009年6月1日月曜日

今日から6月

で、今日から新しい会社勤務。

いきなり電車乗り間違えて、遅刻しそうになって駅からダッシュ。この緊張感がたまらない。楽しくコード書き続けます。

2009年5月26日火曜日

UnityとEntityのAttach

第2回 スキャフォールディング機能で軽々DB連携アプリケーション - @IT

まだ2回目だけど、楽しみにしてる連載です。今回の記事で少し気になる部分があったので、実際に動くコードを書いてみました。暇人...。いやいや、お勉強デス!

その気になる部分っていうのが、ページ5の「既存のレコードを変更したい今回のようなケースでは、UpdateModelメソッドを使用する必要がある。」という部分。いや、もちろん、ModelBinderのみでDBに保存なんてしないし、これが間違ってるという分けじゃないけど、単純に出来る出来ないという話なら出来るっていう話です。

EntityKey and ApplyPropertyChanges() - Stack Overflow

なんだかんだと、実際にコードを書いてみたわけじゃなく、たんなる耳年増なだけだと、ちょっとカッコ悪し。

efunity2

Home/Indexが一覧。Editを用意。

efunity3

"Chai"を"Chai XXX"に編集。

efunity4

ちゃんと保存されました。

↓これがObjectContextにアタッチされていないエンティティを使って、DB更新してみるコード。

   public void Save<TEntity>(TEntity entity, Func<TEntity, TEntity> setIDFunc) 
where TEntity : new() { var entitySetName = _dataContext.DefaultContainerName + "." + EntitySetName(typeof(TEntity)); setIDFunc(entity); // 空のエンティティをアタッチしておいて、更新情報はクリア _dataContext.AttachTo(entitySetName, setIDFunc(new TEntity())); _dataContext.AcceptAllChanges(); _dataContext.ApplyPropertyChanges(entitySetName, entity); }

オブジェクトのアタッチ (Entity Framework)

AttachToでObjectContextに空のエンティティをアタッチしておいて、ModelBinderで復元したエンティティの値をApplyPropertyChangesで反映させる流れです。ObjectStateManagerを使うともっと綺麗にできるのかも?

↑このコードはRepositoryクラスに。コレを呼び出すControllerコードは↓。

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, Product entity)
    {
      if (ModelState.IsValid)
      {
        _repository.Save<Product>(entity, e =>
        {
          e.ProductID = id;
          return e;
        });
        _repository.SaveChanges();

        return RedirectToAction("Index");
      }

      return View(entity);
    }

認証、認可、入力検証ははしょってるんですが、何となく雰囲気は伝わるかな~、と思います。データベースはNorthwindで、Entity Frameworkを使ってProducts/Categoriesだけのエンティティクラスを作成してます。

efunity

HomeControllerのIndexが一覧ページになるようにしておいて、Editを追加しただけのプロジェクトで試してます。今回はコレに加えてUnityを使ってDIでObjectContextをRepositoryにコンストラクタインジェクションと、RepositoryをControllerにコンストラクタインジェクションさせるようにしてみました。

Unityを使ったControllerFactoryなんかはMvcContribにもコードがあったりするので、そちらを参考にするのが近道です。

でもMvcContribのUnityControllerFactoryはLifetimeManagerを指定しないので、Resolveの度に新しいインスタンスを作ります(TransientLifetimeManager)。HttpContextにObjectContextを入れておいて、同じコンテキストなら無駄使いしないようにするためのHttpContextLifetimeManagerクラスを用意。「ASP.NET MVC Tip: Dependency Injection with Unity Application Block - Shiju Varghese's Blog」に書かれてる、HttpContextLifetimeManagerを使わせてもらいました。Get/Set/RemoveのoverrideでHttpContextを使うようにしてるだけですね。

ObjectContextのインスタンスがどうなってるのかを確認するコードをHomeControllerに書いて確認。

    INorthwindRepository _repository;

    public HomeController(INorthwindRepository repository)
    {
      _repository = repository;
      _repository.Debug("Constructor");
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      _repository.Debug("OnActionExecuting");
      base.OnActionExecuting(filterContext);
    }

    protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      _repository.Debug("OnActionExecuted");
      base.OnActionExecuted(filterContext);
    }

    public ActionResult Index()
    {
      var httpApp = HttpContext.ApplicationInstance as IUnityContainerAccessor;
      new NorthwindRepository(
        (NorthwindEntities)httpApp.Container.Resolve(typeof(NorthwindEntities))
      ).Debug("Other-1");

      var list = _repository.All<Product>();

      new NorthwindRepository(
        (NorthwindEntities)httpApp.Container.Resolve(typeof(NorthwindEntities))
      ).Debug("Other-2");

      return View(list);
    }

Debug内ではObjectContext.GetHashCode()を出力するようにしてます。コンストラクターで渡されるRepositoryはUnityに任せたもので、OnActionExecutingとOnActionExecutedではコンストラクタで渡されたものを出力し、Index内でそれぞれ(Other-1/2)新しくリポジトリのインスタンスを作成(Resolve)して出力させてます。

TransientLifetimeManagerを使った出力。

Constructor:6658142
OnActionExecuting:6658142
Other-1:5603269
Other-2:50559794
OnActionExecuted:6658142

Otherはそれぞれ違うインスタンスが生成されてますね。

HttpContextLifetimeManagerを使った出力。Other-1/2は全然違うのが出力されますね。

Constructor:60183783
OnActionExecuting:60183783
Other-1:60183783
Other-2:60183783
OnActionExecuted:60183783

Otherも同じインスタンスです。お利口さんです。Global.asaxに以下のコードを追加してUnityに登録してます。

    void InitializeContainer()
    {
      if (_container == null)
        _container = new UnityContainer();

      IControllerFactory controllerFactory = new UnityControllerFactory(_container);
      ControllerBuilder.Current.SetControllerFactory(controllerFactory);

      // Register
      _container.RegisterType<NorthwindEntities>(
        /*
         * ContainerControlledLifetimeManagerを使うとSingleton。
         * デフォルトはTransientLifetimeManager
         */
          new HttpContextLifetimeManager<NorthwindEntities>()
                )
                .Configure<InjectedMembers>()
                .ConfigureInjectionFor<NorthwindEntities>(
                  new InjectionConstructor(
                    ConfigurationManager.ConnectionStrings["NorthwindEntities"].ConnectionString
                    )
                );
      _container.RegisterType<INorthwindRepository, NorthwindRepository>();
    }

とりあえず、今回はProductデータだけを利用したけど、他にもいろいろなテーブルを同じコードで簡単に処理できるように「An Irishman Down Under - Polymorphic Repository for ADO.Net Entity Framework - Keith Patton's blog」に書かれてるようなジェネリックなリポジトリを書いてみようかな~と試してみたんですが、どうですかね。あんまり便利な気がしない。簡単な機能実装ならいいかもしれないけど、この辺は機能ドメイン毎にちゃんと書いた方がいい気がする。ところで、EntitySet名ってエンティティクラスから簡単に取得する方法ってないんですかね。EntityKeyには入ってるみたいだけど、アタッチして無いとnullで取り出せないじゃないですか。今回はずるっこして、エンティティクラス名+"s"として生成してます。

んん~。今回もプロジェクト添付しておくので、動かしてみたい方はどーぞー。あ、データベースファイルは添付してないです。

2009年5月23日土曜日

イケメンはしゃべりも上手い

久しぶりにアメージングで試合。最近になってゴーリーが誰一人これなくなり、代わりにオグさんがやるらしく、プレーヤーが足りなくなる。ほら、ビシッと決めれるプレーヤーがいないと、あれじゃないっすか。

今期になって色々あって全然試合にも出れず、チームにも迷惑をかけてたな。なんてことは、これっぽっちも思ってないんだけどさ。だって普通に勝ち越してるし。いてもいなくても勝敗に関係ないじゃん...。でも前期のギリギリプレーオフに比べれば、勝ち越してる今期のほうが全然気が楽だから、小さい事は気にしない。

9:30開始だから心が折れそうだったけど、試合が久しぶりだったからかワクワクしすぎて目覚ましより早く起きれた。ちょっと嬉しい。今日はいい感じでプレーできる気がする。そんな気がするときはたいていいい感じでプレーできないんだけどね!

レフェリーがタカチさんでなんか不安だったけど試合開始。で、一点目を決めたんだけど、タカチさんなかなか笛を吹かず、こっち見てる。あれ?入ってるよ?なんで認めないんだ!早く笛を吹いてオレの得点だとコールしたまえよ!イヤそうな顔しながらしょうがなくコールするレフェリー...。久しぶりなんだからもう少し優しくして...。

2点目をセト君のが決めたんだけど、ここで事件発生。セト君は開始時間に間に合って無くて、背番号空白でロースターだしてたのをすっかり忘れてた。あちゃ~。ベンチマイナーで無駄にキルプレーっす。それは自分の責任なのでしっかりキープして時間を使い守りきれました。あぶね~。

でも、なんやかんやと前半結構点を取られまして。そもそもシュートを打たせたらダメなのに、守り方間違えてた。難しいな~。前半終わったところで2-4。負けてるし。後半しっかり攻めて点を取ればいいから、これ以上失点さえしなければ逆転できる。気楽に考えてたけど、アメージングが久しぶりなのをすっかり忘れてたよ。滑っても滑っても全然スピードにのらない。ウィールがぼろいのもあるけど、滑りかたが戸塚の堅いリンク用の滑りかたになってたみたい。全然相手を振り切れない滑りになってて、無駄に疲れた。ちょっと不安になってきた。

でもそんな不安をシンゴが吹き飛ばしてくれた。前回の試合もそうだったらしいんだけど、あれよあれよと追いつき逆転。シンゴ...。君はそんなしっかり者のプレースタイルじゃなかったじゃないか。ここぞというときに外すのがシンゴスタイルじゃなかったのか?どうやら、たけはら不在の間に、チームを引っ張ってたのはシンゴだったみたいね。おぬし、なかなかのもんだな。それに引き替えたけはらさんは、足で蹴り入れたんじゃないかという疑惑のゴールがあったり。でも、相手のペナルティーによるパワープレーで疑惑を晴らすかのようなディフェンスラインからのスラップで名誉挽回(汚名返上?)。

最終的に相手チームのスタミナ切れで何とか逃げ切って勝てたけど、内容的には反省点の多い試合でした。次回以降もう少し丁寧に大人のホッケーをしないとね。

そうそう、マリちゃんとは初めて一緒に試合出たけど、今日の試合ではマリちゃん含め、なんと全員ポイント!ミカさんもイマイちゃんも、意外なことにミチも!全員にポイントが付く試合はあんまりないけど、それが出来た時は一番嬉しい。最後にちょっとイマイちゃんがケガしちゃって、最後までプレー出来なかったが残念でした。

話は変わるんだけど5/20にボーランドソリューションセミナーっていうのがあってそれに行ってみた。無料だったし。でも、個人参加は一人だけで、少し場違いな気がしなくもなく...。テストツールに興味があったからというより、ケースケがしゃべるってことなんで行ってみた。まるで追っかけ。ファンにはたまらない。無料イベント最高!いやいや、そうじゃなくて、ケースケがしゃべる内容がRIAってことだったんで行ったわけです。RIAの話もリサーチした数値の説明があって現状どういうとらえ方をしてるのかを分かりやすく説明してくれたり、今後もっといろんなデバイスにRIAを展開していく話とか、デザイナーとデベロッパー間での効率を良くするためのCatalystのデモをやったりと盛りだくさんで面白かったよ。でも、一番面白かったのは市ヶ谷の駅前で迷子になって地図とにらめっこしてるたけはらさんをケースケが見つけて会場までつれてってくれたところだね!ちなみに駅前迷子の前に、電車の乗り換え間違えて全然違う方向に向かってて途中慌てて乗り換えてなんとかたどり着いたという事件があったりなかったり。最近3回東急東横線に乗ったんだけど、そのうち2回電車が遅れるということもあったりなかったり。電車って怖い...。

2009年5月13日水曜日

HandleErrorの使い方

そういえば、今月に入って全然エントリを書いてなかった事に気がついた。人に言われて気がついた。ずっと本を読んでて、頭がプシューってなっててさ~。

そんなこんなで、ASP.NET MVCでHandleErrorを使って、HTTPステータスコードに合わせたエラーページを用意する方法が日本語情報として無いかもとふと思ったので書いておきます。

使い方はアクションにHandleError属性を指定して、web.configのcustomErrorsのモードをOn/RemoteOnlyに設定しておけば、defaultRedirectが表示されるというものです。

  [HandleError]
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

コントローラに指定しても、アクションに指定してもいいですよね。それぞれに指定した場合、コントローラの普通はHandleErrorが優先されます。 例えば、上記コントローラに以下のアクションを追加してみると確認できますね。

    [HandleError(ExceptionType=typeof(InvalidOperationException),View="Exception")]
    public ActionResult ThrowException()
    {
      throw new InvalidOperationException("ぶぶ~");
    }

ViewにThrowExceptionへのリンクを作成して表示させてみたのが↓こちら。

handleerror2

普通に標準のエラーページが表示されます。ちゃんとアクションのHandleErrorにはViewを指定しているにもかかわらず。これをアクションに指定してるほうを優先させるにはコントローラに指定してるHandleErrorの優先順位を下げればOKです。

  [HandleError(Order=1)]
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

↑こんな感じで。Orderは0スタートなので1以上なら何でもいいです。そうすると今度はどういう表示になるか確認。

handleerror1 

グレート!

ちなみにリリース版のVSテンプレートが出力する~/Views/Shared/Error.aspxでは例外情報の表示コードが無くなってるので分かりにくいですが、このページは少し特殊でInheritsが単なるViewPageじゃなく、例外情報が渡されるように型付きViewPageになってます。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
Inherits="System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>" %>

なので、Viewページ内でModelを参照するとアクション内で発生したExceptionが参照出来るようになってます。あら便利。自分でエラーページを定義する場合のInheritsを上記のように変更しておけば、自動で参照できるという寸法です。

ちなみに上記スクリーンショットのViewは~/Views/Shared/Exception.aspxというのを作成して表示してます。

で、ですね、例外を捕捉して専用エラーページを表示するのももちろん必要だと思うんだけど、そこはやっぱりHTTPステータスコードに合わせたエラーページを定義して、レスポンスのステータスコードも合わせた方がサーチエンジンにもエラーが認識できるし、RESTfulな感じがして素敵だと思いませんか。例えば、上記のページの場合レスポンスされるステータスコードはなんになってるか確認してみる。

handleerror3

500ですね。例外だからそれで良し。でも、例えばidを指定してDB検索して一致するデータがない、なんて時は404で返したいな~、なんて思いませんか。

試しに、web.configのcustomErrorsを以下のように変更。

    <customErrors mode="On" defaultRedirect="Errors">
      <error statusCode="400" redirect="/Errors/400" />
      <error statusCode="403" redirect="/Errors/403" />
      <error statusCode="404" redirect="/Errors/404" />
    </customErrors>

で、HomeControllerに以下のコードを追加。

    public ActionResult BadRequest()
    {
      throw new HttpException(400, "Bad Request");
    }

    public ActionResult Forbidden()
    {
      throw new HttpException(403, "Forbidden");
    }

    public ActionResult NotFound()
    {
      throw new HttpException(404, "Not Found");
    }

ErrorsControllerを追加して、HTTPステータスコードに合わせてViewを返すようにします。

  public class ErrorsController : Controller
  {
    public ActionResult Index(int? statusCode, string aspxerrorpath)
    {
      var viewName = string.Format("Error{0}", statusCode);

      return View(viewName);
    }
  }

handleerror4 

で、Home/BadRequestにアクセスしたのが↑。拡大すると見えるけど、200が返ってます。400が返って欲しい。そんなときはViewResultを書き換えてしまいましょう。

  public class StatusViewResult : ViewResult
  {
    public int StatusCode { get; set; }

    public StatusViewResult(ViewResult viewResult)
    {
      ViewName = viewResult.ViewName;
      MasterName = viewResult.MasterName;
      ViewData = viewResult.ViewData;
      TempData = viewResult.TempData;
    }

    public override void ExecuteResult(ControllerContext context)
    {
      context.HttpContext.Response.StatusCode = StatusCode;
      
      base.ExecuteResult(context);
    }
  }

こんな感じのクラスを作成しておいて、Errorsコントローラでこっちを利用するように変更。

    public ActionResult Index(int? statusCode, string aspxerrorpath)
    {
      var viewName = string.Format("Error{0}", statusCode);
      var resStatusCode = statusCode ?? 500;

      return new StatusViewResult(View(viewName)) {
        StatusCode = resStatusCode
      };
    }

で、同じアクションを実行したのが↓こちら。

handleerror5 

うへへ。ちゃんと400が返ってます。そりゃそーだ。

エラー用のViewとして400/403/404の3種類だけ用意してみました。

handleerror6

それぞれの実行結果は省略しときますが、例えばこの状態で405とかエラー発生させたらどうでしょうね。HomeControllerに以下を追加。

    public ActionResult MethodNotArrowed()
    {
      throw new HttpException(405, "Method Not Arrowed");
    }

これにアクセスしてみたのが↓こちら。

handleerror7

ErrorsControllerのIndexアクションでError405をViewとして返そうとするけど、そんなViewは定義してないのでInternal Server Errorが発生して、テンプレートで用意されてる~/Views/Shared/Error.aspxがレンダリングされてます。すばらしいじゃないですか。

と、いうわけで、今回のプロジェクト一式もダウンロード出来るようにしておきます。

最近こんな話題ばっかりで、周りからはブログつまらないという声もちらほら。しょうがないので前に書いてた今は無き「オレがルールだ!」からサルベージしてごまかそうかと計画中。

dotnetConf2015 Japan

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