2009年7月4日土曜日

わんまるとキャプテンわん

横浜開港150周年記念キャラクターのたねまる。最近まで知らなかったけどこんなのいたのか。電車の車体に貼られてるのを見て知った。

たねまるドットコム|横浜開港150周年マスターライセンシーオフィス公式サイト

tanemaru

そして、どうやら彼女がいるらしい...。

で、キャプテンわん。わんまるとか適当な名前で教えられたけど、これも横浜がらみ。ハマスポのキャラクターらしい。

wan

キャプテンわんコーナー

「落ち込んだら走れ!」って言われた。上のページでちょいちょいメッセージが切り替わって、熱血漢なところをアピール。さすがハマスポ。

写真

そして↑これがガッカリなキャプテンの写真(拡大してみるとガッカリ度さらにアップ)。あまりにもガッカリな状況だ...。こんな写真をアップしてたら訴えられるんじゃないかとヒヤヒヤする。前にも書いたかな~、この横浜市の体育協会の広報資料になぜかたけはらさんの写真が載ってるよ!ファンはゲットしときましょう。

そんな話はいいとして、今日は負けられないレギュラーシーズン最終戦。負けてもギリギリプレーオフには行けるらしいけど、あまりにもギリギリだと初戦から強豪と対決になって気が滅入るので、今日の試合は何が何でも勝っておきたいところ。とは言いつつも、最終戦の相手が一番の強豪だったりしないですかね。ファルコンズって...。ワカバヤシ兄弟で出るって空気読めてないゴールド戦士には出場をご遠慮願いたい。朝一8時30分開始の試合だからあわよくば来てくれるなと思ってたけど、ちゃんといた。「S40と木場マーボーズの試合だけは絶対出る」と本人談。君たちはアレだろ、消化試合というか、調整試合だろ。こっちはプレーオフかかってんだ。もうちょっと空気読んでくれよ。とりあえずケースケは来て無くていなくてホッとした。

ところで、前回の試合の感想を宿題にしといたんだけど、その提出がなかなか無くてさ。ミサキ姫に問い詰めたら、今回の試合と合わせて2試合分をまとめて提出すると、自分でハードル上げてきた。若さって怖い。平成生まれって強い。

試合内容はね~、あれっすよ、大接戦っすよ。試合開始早々相手のペナルティー(やられた身としてはそうでもなかったけど、ここはありがたくチャンスを頂戴しとく)で、パワープレー。シンゴ→タケ→オグでバシッと決めて、さい先のいい試合展開。聞いた話によると、相手ゴーリーはウォールがかかってるらしい。今日の試合次第でベストゴーリーになるほどの鉄壁君だって。それが、序盤そうそうに失点しちゃったもんだから、気持ちも折れるってもんですよ。やったね。そっから、取りつ取られつの試合展開で、あ!っという間に3分(早っ)。まだ同点だったけど、ここで点を取れればというところで、セト君が決めてくれて1点リードで逃げ切り体制。に、持って行きたかったけど、直後のフェイスオフでツトム君に真ん中から突破されてわずか8秒で再度同点。なんじゃそれ!でも、最後はオグさんがバシッと決めて見事な勝利。もう満足。ぶっちゃけプレーオフもういいじゃん、ってくらい出し切った。他の試合結果次第だけど、カネコさんがウォールになりそうなのもちょっと楽しみ。

そもそも、今日もうちはゴーリーがいないはずで、順番で自分がやるはずだったんだけど、どうしてもやりたくなくてタクちゃんに無理を言ってお願いしてきてもらったんだよね。しかも2週間前から連絡を取って、なんとか都合をつけてもらうという周到ぶり。おかげで勝てたよ!タクちゃんのグレートセーブで何度も助けられたし、シンゴの必死の守りで後半2失点で抑えられたのがよかった。と、言うことは前半の自殺点2点が無ければもっと楽に勝てたってこと?誰だ自分たちのゴールに蹴り込んだの!

...すいません。シンタロも一緒に謝っとけ。

久しぶりに7得点で7ポイントの大活躍な自分に拍手。

2009年6月28日日曜日

こんな週もあるか

いや~、疲れた。朝からずっとスケート履きっぱなしはいつ以来だろ。レフェリー自体がずいぶん久しぶりだった気がするな~。なによりノブヒコにあうのは2年ぶり?でも、久しぶりに一緒にホッケー出来て楽しかった。そんな久しぶり続きで箸も持てないほど疲れたけど、1試合も勝てなかったっていうのが一番の驚きかも。全然負けてる気がしないのに、結果2敗2分け?すんごい面白かったから、いいんだけどさ。なんか、こう、あれれ~、な感じだよね。

Google Page Creatorの移行をさっさとすませろと督促メールも来たことだし、サーバー引越中...。なんかいろいろやることあるな~。

2009年6月21日日曜日

RouteHandlerはシングルインスタンス

いや~、サブジェクトの件、全然知りませんでした。

ASP.NET MVCはControllerクラスのAction実行でViewを返すじゃないっすか。ControllerはDefaultControllerFactoryがインスタンスを作るんですよね。で、IControllerさえ実装してればそれはつまりControllerであると。なので、IController.Executeだけを実装したサンプルを書いて、まずはそれを確認。

using System;
using System.Web.Mvc;

namespace RouteHandler.Controllers
{
  public class SimpleController : IController
  {
    private DateTime _createTime = DateTime.Now;
    public void Execute(System.Web.Routing.RequestContext requestContext)
    {
      requestContext.HttpContext.Response.Write(string.Format(@"<html>
<body>
<h1>Simple Controller</h1>
アクションやら完全無視!<br />
{0}
</body>
</html>", _createTime));
    }
  }
}

簡単なコードです。特にこれと言って変なところは無いですね。ただ、こういう使い方をするとActionなんて無いし、この中に定義したとしても、RouteDataにアクションへの情報を参照してInvokeするコードが無いので無視するだけです。ようはActionInvokerに相当する機能がないって事です。

わざわざ_createTimeを取っているのは、この後の確認作業で理由が判明する予定。SimpleControllerという名前なので、デフォルトのルーティング設定だけで、このコントローラは実行まで行くことが出来ます。動かしたのが↓この画面。

route1

画面を拡大してみると分かると思うけど、URLは単純に/simpleです。で、ですよ、Controllerは単純にIHttpHandlerなんだから今度はIHttphandlerを実装してしまえば、DefaultControllerFactoryを経由させないでも出来るってことデスよ。でも、その為にはRouteHandlerが必要になります。いきなりの展開で話が分かりにくいっすね。

ルーティングモジュールがFrontControllerとしてすべての要求を全部受け入れてくれるじゃないですか(そういう言い方しないですかね)。で、その中で登録されてるルーティング情報に従ってRouteDataに色々なデータを突っ込んでくれる。RouteCollectionExtensions.MapRouteが色々セットアップしてUrlRoutingHandler(UrlRoutingHandler クラス (System.Web.Routing))を使ってそこから先はMVCの方で好きなようにやりたまえと責任が委譲されます。MvcRouteHandlerとMvcHandlerがその後を引き継いで、ごにょごにょと進んでいくけど、UrlRoutingHandlerの所を自分で実装したものに差し替えちゃえば、ControllerFactoryなど経由せずIHttpHandlerを直に使えるっていう流れなんですが、こうなるとMVCじゃなくてただのRouting利用っすね...。まぁ、いいや。UrlRoutingHandlerはIRouteHandlerの実装なので、それを実装。

using System;
using System.Web;
using System.Web.Routing;

namespace RouteHandler.Libraries
{
  public class SimpleRouteHandler : IRouteHandler
  {
    private DateTime _createTime = DateTime.Now;

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
      var routeData = requestContext.RouteData;
      routeData.DataTokens["routeTime"] = _createTime;

      var httpHandler = new SimpleHttpHandler(requestContext);
      return httpHandler;
    }
  }
}

ただGetHttpHandlerを実装するだけデス。返すハンドラはSimpleHttpHandler。

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

namespace RouteHandler.Libraries
{
  public class SimpleHttpHandler : IHttpHandler
  {
    private DateTime _createTime = DateTime.Now;
    private DateTime _routeTime;
    
    public SimpleHttpHandler(RequestContext requestContext)
    {
      var routeData = requestContext.RouteData;
      _routeTime = (DateTime)routeData.DataTokens["routeTime"];
    }

    public void ProcessRequest(HttpContext context)
    {
      context.Response.Write(string.Format(@"<html>
<body>
<h1>Simple RouteHandler</h1>
HttpHandlerを直接使う<br />
RouteHandler : {0}<br />
HttpHandler : {1}<br />
</body>
</html>", _routeTime, _createTime));
    }

    public bool IsReusable
    {
      get { return false; }
    }
  }
}

ProcessRequestを実装しただけですが、せっかくRouteHandlerは自前なのでRouteDataを参照したいじゃないですか。なので、コンストラクタでRequestContextを受け取るようにしておいて、RouteDataを取り出しておきます。このRouteHandlerをRouteTableに登録。

      routes.Add(new Route("route", new SimpleRouteHandler()));

Global.asaxのRegisterRoutesに追加するだけです。

これを実行したのが↓これ。

route2 

時間2箇所だしてるので分かるかな~、とは思うんですが、RouteHandlerが生成された時間と、HttpHandlerが生成された時間が少しずれてます。もう少し時間をおいて試したのが↓こっち。

route3

HttpHandlerは毎回違う時間(IsReusable=falseかどうかは見ずに毎回newしてる)になってるけど、RouteHandlerは1個前の写真の時間と同じです。ピンと来た?ちなみに最初のSimpleControllerは毎回時間が更新されます。

そうなんですよ、RouteHandlerのインスタンスは使い回されるんですよ。しかも沢山のスレッドから同時に。なので、RouteHandlerの中でスレッドセーフじゃないクラスを使ったりするとちょっと切ない思いをすることになります。データベースにアクセスしようとかしたらダメですよ...。ロックなんてしたら、これまたスケールしなくなるし。

RouteHandlerにはシンプルなロジックだけを書くようにしましょう、という話でした。

2009年6月20日土曜日

暑すぎだね~

何となく、最近週末にしかエントリ出来てない気がするな~。新人だからそれもやむなし。ボスには好きなだけ(情報漏らさない限り)書いていいと言われてるけど、なかなか眠しなウィークデイっす。今日は珍しく午後から試合ってことで、朝はのんびり過ごしてたんだよね。そろそろ行くかと外に出たら、アレだね、夏だね。オレ in サマーだね。

前回の試合でウィールがひどくて滑れないと、さんざん言い訳をかましてたけど、今回はウィールも新調し、ブレードもおニューで、もう道具のせいには出来ないっていう状況を用意しました。なんせ今回の相手はヤマちゃんもいるチームだしね!うひゃひゃ。この時期恒例の首都高での気温チェックをしてみたら28度になってて、途中行くのやめてマジで帰ろうかと心が折れかかった。

そういえば、iPhone OS 3.0出てるじゃないですか。速攻でアップグレードしたんですよ。いろいろ新機能があったりしてるみたいだけど、やっぱり機械を新しいのにしないとベストな状態で使えてる気がしない。なんとなくレスポンス悪かったりしてる感じが否めない。日本語入力の反応(変換候補の選択が特に反応悪し)悪くないですか?それでも、コピペが出来るのは嬉しいし、曲順が日本語順だったり、細かい表示が改善されてたりして、凄くいいのは確かっす。テザリングが標準で使えないのが残念だけど、最近はめっきりノートPC持ち歩かないから、そこはまぁ、いっか、みたいな。出来るに越したことはないけど、調べたりしてまで使いたいかっていうとそれほどでもないか。

最近、電車通勤してて気がついたんだけど、今履いてるNIKE FREEは少しでも雨が降るとすぐにしみてきて、靴下がびちょびちょになる。もう半年くらい履いてるのに、全然知らなかった...。長靴買おうかな。

そうだ、ホッケーの試合の話だった。なんだかんだヤマちゃんもショウ君もいなくて、ありゃりゃだったけど、ゴーリーが鬼のようなセービングで全然点が取れず、まさかの敗北かと思ったけど、なんとかオーバータイムで勝利。危なかった。53本うって5点っすよ...。どんだけ止めれば気が済むんですか。で、この試合の内容はミサキ選手が後日感想文を送ってくることになってるので、詳細はこうご期待!

試合後、サヨちゃんがいたから「スズキ君は~?」って尋ねたら「10日も仕事休んだから今日は仕事に行ってる~」だって。スゴイよね、世界選手権だもんね。日本代表だもんね。「じゃ~、無事帰ってきたんだね。ケガもなく。」って言ったら「いや、それが...」と、どうも大変な事になってたみたいで、軽く記憶が飛んだりするような危ない感じの脳しんとうを起こしたらしい(とても危険な反則をされたみたい)。昨日は元気に戸塚でまたホッケーしてたらしいけど。今度ちょっと話を聞かせてくれたまえ。

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がレンダリングされてます。すばらしいじゃないですか。

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

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

2009年4月30日木曜日

VirtualPathProviderを使ってデータベースからViewを読み込む

Scripting ASP.NET MVC Views Stored In The Database

PhilさんのブログではIronRubyのViewEngineを使って、データベースから読み込んだViewを出力するサンプルが公開されてたけど、そこはシンプルに普通のASPXの出力が欲しいところ。でも、そもそもファイルの実体が無くても処理出来るということに驚きですが。

以前ViewEngineの実装をしようとしてて、実体がないと上手く行かないな~、っていう理由で諦めたんですがVirtualPathProviderを使う事で解決出来るんですね。ASP.NETそのものの仕様をきちんと理解してなかったです...。

ASP.NET MVCのソースをみると、標準のViewEngine(WebFormViewEngine)はVirtualPathProviderを含むVirtualPathProviderViewEngineから派生し、WebFormViewをインスタンス化する際にBuildManager.CreateInstanceFromVirtualPathを呼び出してます。

VirtualPathProviderっていうのが、パスの指し示す場所にあるファイル(VirtualFile)かディレクトリ(VirtualDirectory)を返す役割を実装します。ファイルはVirtualFile.Openでストリームを返せばいいので、そのストリームをローカルHDDから読み込もうとDBからだろうと、なんかしら別のサービスから取得(なんならHTTPで別サーバーから)取得しようがお構いなしでOKっていうグレートなクラスになってます。

今回この機能を使って、特に変わったViewEngineではなくWebFormViewEngineをそのまま利用して、VirtualPathProviderがデータベース参照するようにしたものを作ってみました。

まずはどこから手をつけていい物やらよく分からないのでPhilさんのサンプルを眺めるところから。が、ViewEngineが違うからコードが少し多いし、ScriptRuntime使うからなんかちょっと複雑。ふにゅ~。

とりあえず、まずはデータベース作ろう。んで、Repository書いてしまおう。

fromdb1

fromdb2

こんな感じのテーブルでLINQ to SQLを使ってモデルを作成。 Idはオートナンバーの主キー。イヤマジでそれじゃおかしい使い方をしてるんだけど気にしない。 ViewNameが名前。ホントはコレが主キーデスね。 VirtualPathが仮想パス。そのままかよ!ここにパスを入れておいて、そのパスへのアクセスを横取りしてDBからファイル内容を返します。 Contentsにファイル内容。ChangeStampは使ってないけど癖で入れちゃってます。

namespace FromDB.Core
{
public interface IFromDBRepository : IDisposable
{
  IQueryable<VirtualView> All();
  VirtualView GetById(int id);
  VirtualView GetByViewName(string viewName);
  VirtualView GetByVirtualPath(string virtualPath);
  void Create(VirtualView model);
  void SubmitChanges();
}
}
リポジトリはこんなインターフェース。単純に全部取得、1件取得(3パターン)、新規、保存だけね。
  public class FromDBRepository : IFromDBRepository
{
  FromDBDataContext _context;

  public FromDBRepository() : this(new FromDBDataContext()){}
  public FromDBRepository(FromDBDataContext context)
  {
    _context = context;
  }

  public void Dispose()
  {
    if (_context != null)
      _context.Dispose();
  }

  public IQueryable<FromDB.Models.VirtualView> All()
  {
    return from view in _context.VirtualViews
           orderby view.Id
           select view;
  }

  public FromDB.Models.VirtualView GetById(int id)
  {
    return All().Where(v => v.Id == id).FirstOrDefault();
  }

  public FromDB.Models.VirtualView GetByViewName(string viewName)
  {
    return All().Where(v => v.ViewName == viewName).FirstOrDefault();
  }

  public FromDB.Models.VirtualView GetByVirtualPath(string virtualPath)
  {
    return All().Where(v => v.VirtualPath == virtualPath).FirstOrDefault();
  }

  public void Create(VirtualView model)
  {
    _context.VirtualViews.InsertOnSubmit(model);
  }

  public void SubmitChanges()
  {
    _context.SubmitChanges();
  }
}
今回もDIは使ってないデス。とりあえず、ココまで出来た段階でHomeControllerをガッツリ書き換えて、このデータを編集する機能にしてしまいます。コードは省略。ダウンロードしてから見てくだい。

fromdb3

↑Indexが一覧。

fromdb4 fromdb5

新規と編集が出来るように。コードエディタはPhilさんみたいにカッコイイのを使わずに、Html.TextAreaでごまかす。いいじゃん! そうそう、このやり方だと、Htmlタグをポストすることになるので、POSTを受け取るアクションのActionFilterで[ValidateInput(false)]を指定するのを忘れずに。 さてさて、ここまでは普通のASP.NET MVCでの作業。ここからVirtualPathProviderの実装に移ります。すったもんだの末、以下の2つがあればいいことが分かりました。 まずはVirtualPathProviderを派生させたクラス。
  public class FromDBVirtualPathProvider : VirtualPathProvider
{
  private VirtualView GetVirtualView(string virtualPath)
  {
    VirtualView vv;
    using (var rep = new FromDBRepository())
    {
      vv = rep.GetByVirtualPath(VirtualPathUtility.ToAppRelative(virtualPath));
    }

    return vv;
  }

  private bool IsVirtualPath(string virtualPath)
  {
    var path = VirtualPathUtility.ToAppRelative(virtualPath);
    return path.StartsWith("~/FromDBViews/", StringComparison.InvariantCultureIgnoreCase);
  }

  public override bool FileExists(string virtualPath)
  {
    if (IsVirtualPath(virtualPath))
      return GetVirtualView(virtualPath) != null;
    else
      return base.FileExists(virtualPath);
  }

  public override VirtualFile GetFile(string virtualPath)
  {
    if (IsVirtualPath(virtualPath))
    {
      var vv = GetVirtualView(virtualPath);

      return new FromDBVirtualFile(virtualPath, vv.Contents);
    }
    else
    {
      return base.GetFile(virtualPath);
    }
  }

  public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
  {
    if (IsVirtualPath(virtualPath))
      return null;
    else
      return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
  }
}
必要なのはFileExistsとGetFileとGetCacheDependency。GetCacheDependencyをオーバーライドしとかないと、実体フォルダもファイルもないのに、ファイルの書き換えが発生しないかどうかのフォルダ監視をスタートさせようとするので実行時にエラーになります。 とりあえず、nullを返しておくようにしてるけど、ここはSqlCacheDependencyとかちゃんと使うのがいいと思います。ちなみに、動かすと分かるんだけどnullを返すとキャッシュしたVirtualFileを使いまわされてちょっと不便。 あと、FileExistsとGetFileの2箇所で無駄にデータベースに問い合わせてるのもキャッシュを使って回避するようにしないとダサイですね。 ~/FromDBViewsフォルダにファイルがあるように見せかけるようにしてます。

次に、データベースから読み込んだデータをファイルだと偽るために、VirtualFileの派生クラスを実装。

  public class FromDBVirtualFile : VirtualFile
{
  string  _contents;

  public FromDBVirtualFile(string virtualPath, string contents)
    : base(virtualPath)
  {
    _contents = contents;
  }

  public override System.IO.Stream Open()
  {
    MemoryStream stream = new MemoryStream();
  
    using (var writer = new StreamWriter(stream))
    {
      writer.Write(_contents);
    }

    return new MemoryStream(stream.GetBuffer());
  }
}

データベースからの読み込みはFromDBVirtualFileProviderで行ってるから、 ここでは文字列をStreamにするだけです。エンコーディング手抜きでサーセン。

あと忘れずにやっておかなきゃいけないのが、VirtualFileProviderの登録。これはGlobal.asaxのApplication_Startで。

    protected void Application_Start()
  {
    RegisterRoutes(RouteTable.Routes);

    System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(new FromDBVirtualPathProvider());
  }

コレでViewの実体ファイルが無くてもデータベースから読み込んだデータを使ってViewを表示することが出来るようになります。最後にFromDBControllerがこのViewを表示するようにします。

  public class FromDBController : Controller
{
  public ActionResult Dynamic(string viewName)
  {
    ViewData["Message"] = "動的ページ生成 from Database";

    return View("~/FromDBViews/" + viewName + ".aspx");
  }
}

※ルーティング登録もしてます。

fromdb6

コレだけ。 That's it.

ひゃっほ~!

2009年4月24日金曜日

ウエストでブレードが激安

ずっとね、アイスのスケートをベランダに放置してたのを忘れてた。ちょっと、というか尋常じゃないくらい臭くて。そもそも、1年くらい研磨してなかったから、ズルズルに横滑りしてたんだけど、あんまり履かないし、いいかな、なんて。

そしたらね、今日スケート出してたのを思い出して見てみたらね、サビサビだった...。うぬ。雨も日も放置してたし、それもやむなし。

道具は大事にしなきゃ!ってことで、久しぶりにウエストに研磨しにいったらね、ブレードが激安で売ってたよ。1シーズン前のがテーパーもレギュラーも7000円均一。2シーズン前のは同じく5000円均一。

カーブは全部揃ってないってノダさんが言ってたけど、ぱっと見一通り揃ってる風でしたよ。展示品無くなり次第終了なので(とは言ってもまだまだ沢山あったけど)、今のうちに買いだめしとくといいよ!

最新モデルは高いしね。

MySQLとMonoでASP.NET MVC - その2

ということで、続きました。

LINQ to EntityがMonoで使えないとなると、やっぱりLINQ to MySQLを使おうかなというところに戻りますよね。これがまたなんだかんだとがんばってみたんだけど...。ガッカリな結果でした。理由はサッパリ分からない。最初はライセンスの問題なのかな~、とかいろいろ試してみたけどできなかった。

mono1

↑これMySQLとxsp2(Mono)で動かしてるんだけど、ちゃんとログインできてるのが確認できると思います。なのでMembershipProvider経由のDBアクセスは成功ってことです。だけど、Sakilaを使ったサンプルはやっぱりダメで。

mono3 mono2

左がMonoの実行結果で、右が同じ物をVista上で動かしたもの。悲しいな~。エラーの意味が全く分からない。なんでリフレクションのエラーなんだろ...。どうしよっかな~。

ってことで、ふとNHibernateが気になりだした。Monoでも動くし。ドライバはDevartじゃなくてConnector/NETを使えばいいから、無料で構築できるのも魅力的。ただ...、xmlでマッピングはマッピラだ。韻を踏もうとして失敗した。それはいいとして、Fluent NHibernateっていうのがあったのも思い出した。NHibernateを使ったことないんだし、いっそのこと最初からFluent NHibernateでいいじゃない。

Fluent NHibernate

まずはNHibernateを調べなきゃ。近道するためにDime castでNHibernate Episodesを全部見て分かった気になったところでFluent NHibernate。

Getting Started: First Project in Fluent NHibernate

ここでいきなりSakilaを使うのはちょっと難しそう(マッピングの仕方もサッパリ分からないし)ので、チュートリアルに書かれてるデータベースをMySQLに構築して作ってみることにしてみました。何となくリレーションに使う項目名がStore_id/Product_idっていうのがカッコ悪いきがするのでStoreId/ProductIdにしてみる。

んで、言われるがままにコンソールアプリケーションを作って、エンティティ定義して、マッピング定義して...。いざ実行!動かず。泣ける。これだけじゃダメ?そりゃそうですね。接続の設定とか全然してないし。Fluentで出来るのかもしれないけど、よくわかんないのでapp.configに書く。

NHibernate - Learning with Code Samples

でも、これもFluent出来るんだね。まぁ、いいや。そこは早足で後回し。

Fluent NHibernate - Configuration - Chad Myers' Blog -

リレーションのカラム名を変えて作っちゃったおかげで、マッピングに失敗して動かず。面倒なことをしてしまった...。

なんだかんだでEomployeeMap。

  public class EmployeeMap : ClassMap<Employee>
 {
   public EmployeeMap()
   {
     Id(x => x.Id);

     Map(x => x.FirstName);
     Map(x => x.LastName);
    
     References(x => x.Store)
       .ColumnName("StoreId");
   }
 }
ProductMapにも挑戦。
  public class ProductMap : ClassMap<Product>
 {
   public ProductMap()
   {
     Id(x => x.Id);
     Map(x => x.Name);
     Map(x => x.Price);
     HasManyToMany(x => x.StoresStockedIn)
         .Cascade.All()
         .Inverse()
         .WithTableName("StoreProduct")
         .WithChildKeyColumn("ProductId");
   }
 }
最後にちょっと難しそうなStoreMap。
  public class StoreMap : ClassMap<Store>
 {
   public StoreMap()
   {
     Id(x => x.Id);
     Map(x => x.Name);

     HasManyToMany(x => x.Products)
         .Cascade.All()
         .WithTableName("StoreProduct")
         .WithChildKeyColumn("ProductId")
         .WithParentKeyColumn("StoreId");

     HasMany(x => x.Staff)
         .KeyColumnNames.Add("StoreId")
         .Cascade.All()
         .Inverse();
   }
 }
たったこれだけのマッピングにえらい時間がかかってしまいました。もっと簡単にAccess.As~CaseFieldでできるのかもしれないけど、ここもスキップして実行!

mono4

おぉ~。チュートリアルと同じ結果だ。そりゃそうだ。今度はコレをASP.NET MVCで実装。すったもんだの末に動くようになりました。

mono5 mono6

mono7 mono8

ぱっと見、普通ジャンって思えるけど、コレ実行環境はxsp2。Mono上で動かしてるんですよ!感動ですよ!もちろんNHibernate.Linqでアクセス!

これだけだと、Fluent NHibernate(r453で試しました)は含まれて無いし、データベース定義も入って無いから動かないけど、その辺はチュートリアルを見ながら試してみて下さいまし。

一応、ダウンロードできるようにして置きます。興味があるならどうぞ。とりあえず、コードを説明。

namespace MonoTest.FluentNHibernate.Controllers
{
 [HandleError]
 public class HomeController : Controller
 {
   IStoreRepository _repository;

   public HomeController()
   {
     _repository = new StoreRepository();
   }

   protected override void Dispose(bool disposing)
   {
     base.Dispose(disposing);
    
     _repository.Dispose();
   }

   public ActionResult Index()
   {
     var list = _repository.AllStore();

     return View(list);
   }

   public ActionResult Details(int id)
   {
     var store = _repository.GetById(id);

     return View(store);
   }

   public ActionResult Edit(int id)
   {
     var store = _repository.GetById(id);

     return View(store);
   }

   [AcceptVerbs(HttpVerbs.Post)]
   public ActionResult Edit(int id, FormCollection forms)
   {
     var store = _repository.GetById(id);
     TryUpdateModel<IStore>(store, forms.ToValueProvider());

     if (!ModelState.IsValid)
       return View(store);

     try
     {
       _repository.Save(store);

       return RedirectToAction("Index");
     }
     catch{
       ModelState.AddModelError("", "保存に失敗デス");
     }

     return View();
   }

   public ActionResult About()
   {
     return View();
   }
 }
}

まずは、コントローラ。HomeControllerを書き換えてます。普通ですね。はい。データアクセスをRepositoryにしといたくらいです。でもDI使ってないデス。

ViewはMonoで動かすのに型付きには出来ないので、スキャッフォールディングで作成(コンテキストメニューのAdd View)した後に、PageディレクティブのInheritsは修正。さらに、LINQ to SQLでもLINQ to Entityでもないので、ID列も自動でリンクに反映されないのを修正。

Repositoryは↓これだけ。

  public interface IStoreRepository : IDisposable
 {
   IQueryable<Store> AllStore();
   Store GetById(int id);
   void Save(Store store);
 }
  public class StoreRepository : IStoreRepository
 {
   ISession _session;

   public StoreRepository() : this(DbSession.GetSession()) { }

   public StoreRepository(ISession session)
   {
     _session = session;
   }

   public IQueryable<Store> AllStore()
   {
     return from store in _session.Linq<Store>()
            select store;
   }

   public Store GetById(int id)
   {
     return AllStore().Where(s => s.Id == id).FirstOrDefault();
   }

   public void Save(Store store)
   {
     using (var transaction = _session.BeginTransaction())
     {
       _session.SaveOrUpdate(store);
       transaction.Commit();
     }
   }

   public void  Dispose()
   {
     if (_session == null )
       return;

     if (_session.IsConnected)
       _session.Close();

     _session.Dispose();
   }
 }

データベースのSession(DataContext的な?)は↓こう。

  public class DbSession
 {
   private static ISessionFactory CreateSessionFactory()
   {
     return Fluently.Configure()
         .Database(
           MySQLConfiguration.Standard.ConnectionString(c =>
             c.FromConnectionStringWithKey("MySqlServices")))
         .ExposeConfiguration(c => c.Properties.Add("use_proxy_validator", "false"))
         .Mappings(m =>
             m.FluentMappings.AddFromAssemblyOf<DbSession>())
         .BuildSessionFactory();
   }

   public static ISession GetSession()
   {
     return CreateSessionFactory().OpenSession();
   }
 }

この辺、どうやって作るのがいいのかまだよく分かってないけど、とりあえずは動くものってことで。

Fluent NHibernateをORMに使って、データアクセスにはLINQ、データベースはMySQLでドライバはMySQL.Data(Connector/Net)。実行環境はCent OS 5.3上のMono 2.4。完全にフリーで動かす環境で、ASP.NET MVCを使ったプロダクト開発が出来ることが分かりました!過負荷の時にも上手く動き続けるのかは今後の課題。運用時にはxspじゃなくてApache+mod_monoで動かしましょう。

で、ここまでやってやっとSharp Architectureの良さに気がつくわけですね。

sharp-architecture - Google Code

Sharp Architectureを使うと、ASP.NET MVC用のヘルパーがちゃんと用意されてて、NHibernateが更に使いやすいみたい。T4 Toolboxでのマッピングの自動生成や、Sessionを管理出来るTransactionAttributeなんかがあるし、プロジェクトのフォルダ構成なんかがきちんと区切られてて、もっともっとちゃんと調べて使えるようになれば、Windows環境だけじゃなく、Monoにもデプロイ出来るプロジェクトを綺麗に作れること間違いなし。

#75 - Introdction to S#arp Architecture

Dime Castsにもビデオあるから、これも興味あれば見てみるといいと思います。10分くらいだし。

しかし、分からないことだらけで、時間かかりすぎたな~。もっと、サクッと動くコードが書けると思ったのに。

追記

Monoで動かす際に、System.Web.Mvc/System.Data.Servicesはアセンブリのローカルコピーを忘れずにね。

MySQLとMonoでASP.NET MVC

思い出す度にいろいろ試してみたけど、どうにもこうにも...。

Using MySQL with Entity Framework and ASP.NET MVC – Part I « Christopher Patterson

ホントにConnector/Net 6.0でデザイナーつかってORマッピングの定義できるの~??どんだけやってもできないんだけどな~。とりあえず現バージョンではできないってことにしとこう。自分で定義ファイルを書けばできるんだろうけど、それじゃ~、全然便利じゃないしよくわかんないし。あ、コマンドラインで実行したらできたりするのかな?まぁ、いいや。

なので、有料だけどトライアルもあるし、DevartのdotConnect for MySql(Download dotConnect for MySQL)を使う事にする。流石にコレならできるでしょう。買う事になってもDeveloper Editionで$229.95だしね。個人でも買える値段で助かる(買わずにトライアルだけど)。

まずは、インストール。サクッと終了。チュートリアル「dotConnect LINQ to MySQL Tutorial」を見ながら、コードを書いてみようと思ったけど、どうも"Devart LINQ to SQL Model"のItemTemplateが見つからない。おやや?インストールに失敗したのかな?と思いきや、なんの事はない、VSのテンプレートフォルダを見てみたら1033(英語)には入ってるけど1041(日本語)には入って無かっただけでした。DevartLINQtoSQL.zipを1041フォルダにコピーしてdevenv /InstallVSTemplates。これでプロジェクトに追加出来るようになりました。ちなみにテストで使うデータベースのサンプルをどうしようか。検索してみたらMySqlだとSakilaっていうのがNorthwindやAdventureWorks的なデータベースっぽいのでそれをインストールして使う事にしました。

SakilaSampleDB - MySQL Forge Wiki

で、Sakila.lqmlという名前で作成したので、デザイナーでテーブルをチョイチョイ登録しようと思ったら、外部デザイナーが立ち上がった。

devart1

ん?なんか違和感。そういうものなのかな、と思いつつ、適当にデータベース登録してテーブル追加して作成。

devart2

で?えーと、あ、ProjectメニューにGenerateってある。これをクリックして、なにやらファイルを出力してみる。で、VSに戻ってコンパイル。おぉ~。なんか普通に使える~。

※新しいバージョンで試してみたら、勝手に生成してくれるようになってた。

でも、待てよ。Entity Frameworkってこんなじゃなかったような...。ちゃんと見たら全然違うの作ってた。LINQ to MySqlって書いてるジャン...。やりたかったのはコレじゃなくて"dotConnect for MySQL Entity Framework Tutorial"こっちでした。これはこれで普通に取得やら更新もできたんだけど、リレーションのデータがちゃんと取れなくて、なんでなんで~と頭を抱えて損した。まぁ、いっかと、気を取り直してEntity Framework。今度は間違えないように"ADO.NET Entity Data Model"をちゃんと選んでテーブルを追加。そうそう、これこれ。このウィザードでテーブルを選ぶところでエラーが出て先に進まなかったんだけど、今度はなんの問題も無くクリア。

devart3 devart9

あとは、Scaffoldでちゃちゃっと作る。

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

using System.Transactions;
using MonoTest.Models;

namespace MonoTest.Controllers
{
    public class SakilaController : Controller
    {
      //
      // GET: /Sakila/
      sakilaEFEntities context;

      public SakilaController()
      {
        context = new sakilaEFEntities();
      }

      public ActionResult Index()
      {
        var list = (from act in context.actor
                    select act).Take(10).ToList();
        return View(list);
      }

      public ActionResult Details(int id)
      {

        var actor = (from act in context.actor
                                        .Include("film_actor")
                                        .Include("film_actor.film")
                     where act.actor_id == id
                     select act).FirstOrDefault();
        if (actor == null)
          return RedirectToAction("Index");
        
        return View(actor);
      }

      public ActionResult Edit(int id)
      {
        var actor = (from act in context.actor
                     where act.actor_id == id
                     select act).FirstOrDefault();
        if (actor == null)
          return RedirectToAction("Index");

        return View(actor);
      }

      [AcceptVerbs(HttpVerbs.Post)]
      public ActionResult Edit(int id, actor actor)
      {
        if (ModelState.IsValid)
        {
          using (var ts = new TransactionScope())
          {
            var entity = (from act in context.actor
                          where act.actor_id == id
                          select act).FirstOrDefault();
            if (entity != null && actor != null)
            {
              actor.actor_id = id;
              actor.last_update = DateTime.Now;
              context.ApplyPropertyChanges(entity.EntityKey.EntitySetName, actor);
              context.SaveChanges();
            }
            ts.Complete();

            return RedirectToAction("Details", new { id });
          }
        }

        return View(actor);
      }
    }
}

Controllerはこんなので。

devart4 devart5 devart6

ちゃんと動く~。CentOS側のデータを確認してみる。

devart7

ちゃんと更新できてる~。なんか嬉しい。でも、Monoで動かしてるわけじゃないので当たり前っすね。これをMonoで動かすのが目的なのでxsp2!

devart8

ガッカリです。エラー出て動きませんでした。

ASP.NET Provider Model Support in dotConnect for MySQL

色々試してみたけど、どうやっても動かない。うぎゃ~!!と思って調べて見たら...。MonoではLINQ to Entityが動かないらしい...。凹む。ベコっと音が出た。

続く。

2009年4月19日日曜日

いっぱいいっぱい

4月だけど、もう十分暑いね~。あまりにも人が少ない&オグさんいないってことで、気分転換もかねて久しぶりにアメージングに行ってきました。

海の向こうではNHLのプレーオフが始まってて、NHL.comのトップページにオベチキンガックリの写真が上がってたりと目が離せないね。レギュラーシーズンの録画も全然観れてないんだけど...。レンジャーズが2-0でリードしてるけど、まだまだこれから巻き返せる範囲だからキャピタルズにはがんばってもらいたい。初戦を落としたシャークスもダックス相手にがんばって!レッドウィングスはとりあえず2-0でリードだから連覇を目指してこの調子で勝ってね!

自分達の試合の話は...あんまり覚えてないな~。とりあえずレフェリーがタカチさんでチョイチョイいじられたのだけは覚えてる。ハッキリと。4点目のダブルアシストを指差して除外されたのもハッキリ覚えてますよ!

ぎりぎりまでゴーリーがいないってことで、オグさんゴーリーに引き続き今回はオレがゴーリーやんだろーなーと思ってたら、タクちゃんが来てくれるってことになって、超助かった。試合前から「滑れないから無理」と弱気なタクちゃんだったけど、そんなのどうでもいいっす。もう来てくれただけで助かる。ところでタクちゃんと一緒にいた女性の方は誰なんですかね。気になってしょうがなかったじゃないか。

試合開始前に男子3人女子3人+タクちゃんで、どういうセットにしようか悩んだけど、開始直前にミズノメンが来てくれて男子4人になったから、これまた助かった。これなら常時男2人をリンクに乗せておけるもんね。 試合は開始早々にミズノメンの個人技で2得点。うひょ~。オレいらね。今日はこのままミズノメンになんとかしてもらいたいところデス。

が、ミズノ君と一緒に組んでたテンパ君がゴール前のマークをことごとく外して3失点。アレは無理。止められません。タクちゃんが滑れる滑れない以前に無理。バックドアから触るだけで入るシュートだし。外す方が難しい。テンパがんばって。後輩がベンチから応援してるんだから! たけはらさんはね~、まぁね~、ぼちぼちですよ。普通にブラスに溶け込んでる感じっていうんですかね。あの子がんばってるね、的な?いっぱいいっぱいデス。

とりあえず、ギリギリだけど勝ててよかったね。ここで負けてたら負け越すところだったしね。負け越すといえば、駒沢NEXTにヤマちゃん出てたね。対戦するのかな。楽しみにしとこ~っと。今期から加入のミサキちゃん(?)はなにげに上手い気がする。下のクラスで出てるって話だけど、ブラスでも別に問題ない気がする。何度も惜しいシュートがあったしね。

そういえばS40蒲公英には"ミ"が付く女子が多い気がする。ミカ・ミチ・ミナ・ユミ・ミサキ...。7人中5人もいるね。流行なの?

2009年4月13日月曜日

ASP.NET MVCをMonoで動かしてみる

長い戦いだった...。現時点での結論から言えばそれなりに動くけど、型付きViewPageだけはどうにもならない感じでしょうか。

そもそもMonoでASP.NET MVCを動かすことにどれほどの意味があるのかという所だけど、これは凄く意味のあることですよね。なんせ実行環境はすべてオープンソースで構築できるんだから。こうなってしまえば個人的にはJavaに何もメリットを感じない。Google App Engineが羨ましいくらいか。でもPythonあるし、そもそもAzureがあるし。

構築した環境はVista Ultimate 32bitにVMWare Server 1.0.9を入れ(2.0.1だとどうも上手く動かなかった)、Linuxディストリビューション(っていうの?)はCentOS 5.3openSUSE 11.1、Mono 2.4とMonoDevelop 2.0、データベースにMySQL 5.0.45(CentOS上にyum install)。

My Adventures Installing mono 2.0 on CentOS 4 to work with apache via mod_mono « Pale Musings

Linuxを生まれて初めて触ってみたけど、さっぱりチンプンカンプンですね。でも、ネットで検索すればいくらでも情報が出てくるから、とりあえず動かすくらいならどうとでもなるもんです。ちゃんとまともに運用環境を作るにはかなり難しそうな気がするけどね。

何でCentOSとopenSUSEを入れたかたというと、MonoDevelopをCentOSにインストールするのに3日がんばったけどできなかったから。GLibのバージョンが入ってるのじゃダメと言われ、ソースコンパイルからやってみたけど、芋ずる式にアレもコレもダメだと言われ、コンパイルできてmake installで実行しても、Gtk+からちゃんと新しいのが見えないと言われ...。うぎゃ~!!となってやめました。Monoはすんなり入るのにね。で、一番簡単なのはNovellが出してるディストリビューションを使う事だと思って、openSUSEも入れることにしたんです。まぁ一緒だろと思ったらyumじゃなくてzypper(ソフトウエア管理のコマンドライン比較 – openSUSE)だとかよくわかんないことに時間を取られたものの、流石Novellなだけあってワンクリックでインストールもあっという間に完了。

mono1 mono2

MonoDevelopでASP.NET MVCのプロジェクトを新規作成したところ、流石にテンプレートは空っぽでControllerもViewも何もない状態でした。T4無いしね(たぶん)。なので、簡単にControllerとViewを作って動かしてみると、あら素敵。アッサリ動いた。簡単なものだけど、動くとビックリ。スゴイね~。んじゃ、ってことで、コレをCentOSの方にコピーしよと思ってハタと手が止まる。あれ?こういう時ってFTP?それともSSHっていう何かよくわかんないシェル?ふぬ~。あ、産婆。じゃなくてsamba。

Windowsファイルサーバー構築(Samba) - CentOSで自宅サーバー構築

これをCentOSとoenSUSE両方で起動してコピペ。ちなみにVMWareで動かしてるときにopenSUSEは何もしなくてもマウスが動くしGNOME端末での日本語入力もできるのに、CentOSはなんかやらなきゃいけないみたいで。VMWare toolsっていうんですかね。よくわかんないけど、さんざんやって上手く出来なかったから諦めた。いいもん。毎回Alt+Ctrl押すもん。

とりあえず、MonoDevelopだとVisual Studioに慣れた体では開発しにくいし、どうせ動かすのはCentOSだしってことで、ここヵらはCentOSにデプロイ、Vista+VSで開発に戻ります。openSUSEで動かしてもいいんだけどさ~。なんとなくきかん坊が気になるからさ~。

mono3

VSで作ったデフォルトのプロジェクトをそのままの状態でビルド>発行して、CentOSにコピー。んで、Apacheで動かすためにmod_monoの設定をしようかなってところで、XSPっていうのが簡易Webサーバーの役割を果たしてくれるということなんで、コピーしたフォルダをカレントに"xsp2"!アッサリ動くの図↑。

続いてMembershipを使いたいから、データベースを入れなきゃいけないよね。なのでMySQL。入れるのは簡単。とにかくyumさん。なんだこいつ、スゴイヤツだ。sudoさんと仲がいいのか?最初はsuで切り替えてやってたけど、普通のユーザーアカウントで読み取れないフォルダがばかばかできちゃって面倒なことになったから、結局何度もやり直して須藤さんsudo(SuperUser DO?)に任せる事に。

でもって、MembershipProviderがなんかあるはず。

MySQL :: MySQL 5.1 リファレンスマニュアル :: 24.2.2.2 Mono を使用した Unix に Connector/NET をインストールする

あるのね...。Connector/Netと言われても...。GACに登録したいけど、インストールパスってどこなんでしょう。そもそもどこにインストールされてるのかが分からない。"/usr/lib/mono/2.0"にいろいろ入ってみたいだけどここでいいのかな...。とりあえずコピーしてgacutilでグローバルアセンブリキャッシュに入れて見る。エラーは出てないからいいのかな。

んで"nolan bailey's blog: MySQL ASP.NET Membership and Role Provider"や”knowledge shared is knowledge²: ASP.NET and MySQL - membership provider (part 1)”ここに書かれてるようにweb.configをセット。autogenerateschema=”true”にしておくと勝手にテーブルを準備してくれるからaspnet_regsqlやInstallMembership.sqlをどうのこうの気にしなくてもいいから楽ちんです。忘れずにセットしなきゃね。

mono4

うほほ。ちゃんとデータベースに登録されました。

でも、本当はここが最初ちゃんと動かなくて、あれれな感じだったのがプロジェクトのアセンブリに追加したMySql.Webをローカルコピーしとくようにしたらなんとなく先に進むようになるものの、そこからさらにエラーでなんとも動かなくて。

mono5 mono6

ユーザー登録はできたのに、ログインでエラーになるの図。これはなんだも。

System.ArgumentNullException: Argument cannot be null.
Parameter name: type
 at System.ComponentModel.TypeDescriptor.GetConverter (System.Type type) [0x00000]
 at System.Web.Mvc.DefaultModelBinder.BindModel (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.GetParameterValue (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ParameterDescriptor parameterDescriptor) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.GetParameterValues (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ActionDescriptor actionDescriptor) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.InvokeAction (System.Web.Mvc.ControllerContext controllerContext, System.String actionName) [0x00000] 
なんでこんなエラーが...。全然ダメじゃないか。いろいろ検索してみたら同じ現象が起きてる人もいて。

http://go-mono.com/forums/#nabble-to23006435

特に返信もなくて途方に暮れかかったけど、Forumあるならスレッド見ていけばなにかヒントがあるかもと、見ていくと、↓こんな書き込みがありました。

http://go-mono.com/forums/#nabble-to22854337

Windowsで使ってるアセンブリをローカルコピーしとけと。なるほど。確かにバイナリ互換なMonoなんだからそういう手もありだ。System.Web.Mvc.dllをコピーして動かす。

mono7

拡大すると見えるけど、ちゃんとログインできてますね。グレート!

あとはLINQでデータベースにアクセスできれば普通に開発できる。でも、最初に書いたとおり型付きViewPageだけは今のところ解決策が見つからずなので、当面はコードビハインド指定で乗り切ることになるのかな。そんなこんなでここまで5日ほどかかったけど、面白い発見もいろいろあって実行環境としてLinuxも選択肢に入れていこうと思ったり思わなかったり(EC2もWindows Server+SQLServerよりもLinux+Mono+MySQLの方が同じインスタンスでも安く運用できるからね)。

dotnetConf2015 Japan

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