2010年8月19日木曜日

MVC HTML5 Toolkit を MVC3 Preview1のRazorで使う

ASP.net MVC HTML5 Helpers Toolkit

ナオキさんに勧められたので調べてみました。

ナオキにASP.NET(仮) : MVC HTML5 Toolkit が CodePlex にて公開!

mvchtml5

まずは、上記サイトからソースとAssemblyをダウンロードしてみました。名前空間がSystem.Web.Mvc.Html5というところがムフフなポイントですね。

ソースがあまりにもシンプルで短いので見てみるのをお勧めします。で、見てみてサンプルを動かしてみました。

面白いね~。3大派手機能のうちVideo/Audioのタグには対応。あとはCanvasも同じように作ればいいね!

ベースとなるHtml5TextBoxとHtml5RangeNumberはModelStateを取り出してないから、値がバインドしてこないです。ちょっと残念。InputHelperを使ってるHtml5TextBoxForHelperはViewData.ModelState.TryGetValueでちゃんとバインドしてるから、いろいろ試しながら作ってるのか、意図的にバインドしないようにしてる感じです。input type="range"もバインドしてくれていいと思うけどな~。せっかくhtmlHelper.GetModelStateValueも用意してるんだし。

サンプルのMVCプロジェクトではSite.MasterでDOCTYPE htmlの出力じゃないけど、そんなの気にせずSafari,Chromeはちゃんとレンダリングしてくれるのがすばらしい。

Dean Hume - ASP.net MVC HTML5 Toolkit

ちなみにMVC2用だとうたってますが、そんなの気にせずMVC 3 Preview1で使ってみましょう。作者のサイトに書かれてる通り、参照設定に追加して、Global.asaxにおまじない。

mvchtml5_2

もちろんRazorなので、NamespaceをWebPageからHtmlHelperへの拡張メソッドが参照できるように、Global.asaxに以下のように追記。

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();
  CodeGeneratorSettings.AddGlobalImport("System.Web.Mvc.Html5");

  RegisterGlobalFilters(GlobalFilters.Filters);
  RegisterRoutes(RouteTable.Routes);
}

ページに直接usingとして書いてもいいですが、すべてのページに書くのは馬鹿らしいじゃないですか。Tumblrにも書いたと思いますが、Preview1ではWeb.configのsystem.web/pages/namespacesはみてくれないです。そこはRTMまでには解決されることでしょう。

前回作ったサルベージ用アプリに追加してみる。

@using(Html.BeginForm("Index","Home",FormMethod.Get)) {

  @Html.TextBox("q")

  <input type="submit" value="検索" />

  @Html.Html5Range(1, 100 , 0, 23, null).AsHtml()
}

AsHtml拡張メソッドもがある前提です。

対応してるChromeだと↓こんな感じでちゃんとRangeコントローラが出てきます。

mvchtml5_3

Firefoxだとまだ対応してないので、type属性無視してTextboxがそのまま表示されます。

mvchtml5_4

お手軽でとてもいいじゃないですか!これは楽しい!

あと、個人的に面白いと思うのが↓。

aspnet - Release: Sprite and Image Optimization Framework

地味な感じがするけど、ASP.NETらしさ爆発じゃないですかね~。

2010年8月8日日曜日

サルベージ開始

最近だとMVC3 Preview1がとても興味深く、楽しそうなプロダクトで、あれやこれやいじり倒してみたいところですが、なんか忙しかったり、テンション上がらなかったりとエントリ書く気も起きないな~、なんてぼやく日曜日。

で、思い出したんですよ。昔々「オレがルールだ」なんていうブログを書いていたのを。その時のデータベースファイル(MDF)を発掘したので、そこからエントリをサルベージするってどうなんよ、と。データベースファイルをローカルのSQL Expressにアタッチしたのはいいけど、そのままテキストだけだと画像なんかも見れないし、なんか良く解らん状態。なので、せっかくなのでMVC3P1 Razorで参照専用のWebアプリを書いてみたわけです。書くと言っても参照専用なのでほとんどすることはないんですけどね。EF4CTP1でも使おうかと思ったけど、インストールしてないし、するのも面倒だから、既にインストール済みのPOCOテンプレートを使ってみました。これまた特にすることもなく、EDMX作ってカスタムツール外して、ファイル名をttに埋め込むだけなんですけどね。

なんかかんや、録画しておいてハゲタカTHE MOVIE見ながらチクチクと。

salvage1 salvage2

書いてて気がついたんですが、RazorってデフォルトHTMLEncodeかけるんですね。HTMLをそのまま出力したいときにはどうしましょう...。

とりあえず、以下のようなヘルパーを用意しておくことで簡単に出力できるとStackoverflow.com先生が教えてくれました。

namespace System.Web.Mvc
{
  public static class HtmlHelperExtensions
  {
    public static IHtmlString Literal(this HtmlHelper htmlHelper, string html)
    {
      return MvcHtmlString.Create(html);
    }

    public static IHtmlString AsHtml(this string html)
    {
      return MvcHtmlString.Create(html);
    }
  }
}

Emitting unencoded strings in a Razor view - Stack Overflow

↑これがあれば↓こうですね!

  <div class="entry_view">
    @Html.Literal(Model.contents)
  </div>
  <div class="entry_view">
    @Model.contents.AsHtml()
  </div>

グダグダ前置き長くてすいません。というわけで第1弾「もっと速く出来ると思う」です。


もっと早く出来ると思う

2009/01/16 18:52:29

ここ最近Philさんのブログでホットな話題になってる名前書式化スピード競争。

Fun With Named Formats, String Parsing, and Edge Cases

↑このエントリで始まって、Philさんが「オレのんが速え~」って事でコードを載せてたけど、ここにきて新たな刺客登場。

Named Formats Redux

正規表現だから遅いんじゃないぜ!といわんばかりにJamesFormatterを作成したJamesさん。
おいおいフィル、同じ会社の社員としてそれじゃいかんぜ、とHenriFomatterを作成したHenri Weichersさん。
いずれもPhilさんの速度を超えた。

ルールは簡単。
通常のstring.Format(”書式化文字列”,value)の書式化では引数のインデックスを埋め込むけど、匿名クラスのパラメータ名を渡して書式化出来るようにすること。んで、テストがちゃんと通ること。

例えば
var values = new { foo = 123, bar = 456.7, boo = "Hello!" };
string text = "{foo} {bar} {boo}.";
なら
text = "123 456.7 Hello!."
になる。

これは面白そう。
流石に、速いコードを書く事に慣れてないから苦戦すると思ったけど...。

str

誰よりも速えーじゃねーか。
※順番で不公平(なわけないけど)だ!なんて言われてもイヤなんで、最初と最後の2箇所で。
そんなに難しいコードじゃなくて、テストを通すことと、書式化文字列がもっと長い場合とかは考慮せず、シンプルに書いてみた。

str2

ほら。テストも全通過。
テストツールはNUnit入れてないからVisual Studioに入ってるやつで出来るようにコピペして作り直してるけど、やってるテスト内容は同じ。

コードは↓これ。

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

using System.Reflection;

namespace StringLib
{
  public static class TakeharaNamedFormatter
  {
    public static IEnumerable<KeyValuePair<string, object>> AnonProperties(string prefix, PropertyInfo pi, object inst)
    {
      var piVal = pi.GetValue(inst, null);
      if (!pi.PropertyType.IsValueType && pi.PropertyType.IsNotPublic)
      {
        foreach (var subpi in piVal.GetType().GetProperties())
          foreach (var kv in AnonProperties(pi.Name + ".", subpi, piVal))
            yield return kv;
      }
      else
        yield return new KeyValuePair<string, object>(prefix + pi.Name, piVal);
    }

    public static string TakeharaFormat(this string format, object values)
    {
      if (format == null)
        throw new ArgumentNullException("format");

      List<string> formats = new List<string>();
      Dictionary<string, object> vals = new Dictionary<string, object>();
      foreach (var pi in values.GetType().GetProperties())
        foreach(var kv in AnonProperties("", pi, values))
          vals.Add(kv.Key, kv.Value);

      var len = format.Length;
      string result = format;

      char atchar;
      int sbrace = 0, ebrace = 0;
      string replace_format = "";

      for (int i = 0; i < len; i++)
      {
        atchar = format[i];

        // start
        if (atchar == '{')
        {
          if (replace_format != "")
            sbrace = 0;

          sbrace++;
          ebrace = 0;
          replace_format = "";
        }
        else
        {
          // end?
          if (atchar == '}')
          {
            ebrace++;
            if (sbrace % 2 != 0 && replace_format != "")
            {
              //sbrace = 0;
              formats.Add(replace_format);
            }
          }
          // format string?
          else
            if (sbrace > 0 && ebrace == 0)
              replace_format += atchar;
            else if (((sbrace + ebrace) % 2 != 0) && ebrace != sbrace)
              throw new FormatException();
        }
      }

      if ((sbrace + ebrace) % 2 != 0)
        throw new FormatException();

      // replace!
      var value_format = "";
      foreach (var f in formats)
      {
        var fa = f.Split(':');

        if (!vals.ContainsKey(fa[0]))
          throw new FormatException();

        if(fa.Length == 2)
          value_format = "{0:" + fa[1] + "}";
        else
          value_format = "{0}";

        result = result.Replace(string.Format("{{{0}}}", f), 
                                string.Format(value_format, vals[fa[0]]));
      }

      result = result.Replace("{{", "{")
                     .Replace("}}", "}");

      return result;
    }
  }
}

 

リフレクション使ってるし、ジェネリックも使ってるけど、遅くないよね...。
もっと速いコードが出てきたら、また考える。


いや~、コピペって楽ですね!

ストックが1813件あるということは、しばらく...。そんなわけないですね。ほとんどがどうでもいい話だったりするので、これなら今公開しても、まぁいいかなというのを、ちょいちょい探してサルベージしていこうかと思う次第です。

2010年7月25日日曜日

Data ServicesのPOCO利用

ADO.NET Dataservice/WCF Data ServicesってそれぞれEntity Frameworkを使う場合とPOCOを使う場合と、それぞれを混在させたい場合とあるような気がするけどどうなんでしょう。Read Onlyなら普通に混在できていいんじゃないかと思うけど簡単に出来たりしないのかな~。

データ モデル (ADO.NET Data Services フレームワーク)

試しにNorthwindのProducts/Categories/Order/Order Detailsでやってみた。

まずはVS2008を使ってADO.NET Entity Data Modelを以下のように作成。

astoria1

続いてADO.NET Data Serviceを作成。

 

public class DataEF : DataService<NorthwindEntities>
{
  public static void InitializeService(IDataServiceConfiguration config)
  {
    config.UseVerboseErrors = true; // 追加

    config.SetEntitySetAccessRule("*", 
EntitySetRights.AllRead); config.SetServiceOperationAccessRule("*",
ServiceOperationRights.AllRead); } }

astoria2

普通ですね。

これに以下のようなPOCOクラスも混在させたい。

public class DailyOrderSummary
{
  public int Year { get; set; }
  public int Month { get; set; }
  public int Day { get; set; }
  public decimal Amount { get; set; }
}

このモデルを返すのは以下のようなクエリです。

from o in Context.Orders
where o.OrderDate.HasValue
group o by new 
{ 
  o.OrderDate.Value.Year, 
  o.OrderDate.Value.Month, 
  o.OrderDate.Value.Day 
} into daily
select new DailyOrderSummary
{
  Year = daily.Key.Year,
  Month = daily.Key.Month,
  Day = daily.Key.Day,
  Amount = daily.Sum(d => d.Order_Details
                           .Sum(od => (od.UnitPrice * 
                                       od.Quantity)))
};

がしかし、これを利用するためにDataEfクラスにIQueryableなメソッドを追加してもどうにもこうにも機能してくれないんですよ。Object Service層にはそんなのいないからってことでしょうね。んじゃどうしましょうか。

An ADO.NET Data Services Primer - O'Reilly Answers

ここにあるように一旦POCOだけで動かせる状態にして、そのクラスをプロキシとしてDataService<T>を作ってみます。

public class EfDataModels
{
  public NorthwindEntities Context { get; private set; }

  public EfDataModels()
  {
    Context = new NorthwindEntities();
  }

  public IQueryable<Categories> Categories
  {
    get { return Context.Categories.AsQueryable(); }
  }

  public IQueryable<Products> Products
  {
    get { return Context.Products.AsQueryable(); }
  }

  public IQueryable<Orders> Orders
  {
    get { return Context.Orders.AsQueryable(); }
  }

  public IQueryable<Order_Details> OrderDetails
  {
    get { return Context.Order_Details.AsQueryable(); }
  }

  public IQueryable<DailyOrderSummary> DailyOrderSummaries
  {
    get
    {
      return 
         from o in Context.Orders
         where o.OrderDate.HasValue
         group o by new
                      {
                        o.OrderDate.Value.Year, 
                o.OrderDate.Value.Month, 
                o.OrderDate.Value.Day
                      }
           into daily
           select new DailyOrderSummary
           {
             Year = daily.Key.Year,
             Month = daily.Key.Month,
             Day = daily.Key.Day,
             Amount = daily.Sum(d => d.Order_Details
                        .Sum(od => (od.UnitPrice * 
                          od.Quantity)))
           };
    }
  }
}

これを使ってDataEfを定義することにしてみたけど、これはこれでやっぱりエラー。

astoria3

でもね、上記EfDataModelクラスからEntity Modelで定義したものを除外してDailyOrderSummariesだけにするとうまく行くわけですよ。

astoria4 astoria5

となると、違いはDataService<T>に指定するクラスじゃなくて、モデルの基底クラスなのかなとなりますね。なりませんかね。ソースかドキュメントでもあれば自信を持って基底クラスがEntityObjectのものがいるとPOCO混在出来ないと言えるところなんですが、そのへんよくわからないです。教えてエライ人!

で、それならそれでLINQ to SQLのDataContextを使うという方法もありますね。こっちは基底クラスがいない(objectな)わけで。最初に戻ってLINQ to SQLのDataContextの作成からやり直してみます。

astoria6

EFと一緒です。あ、そうそうちなみにPOCOの場合、キー項目がわからないので、クラスにDataServiceKey属性を付けるのを忘れずに。

ADO.NET Data Services and LINQ-to-SQL - Sergey Zwezdin

なので、先のDailyOrderSummaryクラスは以下のように属性を付けておく必要あり。

[DataServiceKey("Year", "Month", "Day")]
public class DailyOrderSummary
{
  public int Year { get; set; }
  public int Month { get; set; }
  public int Day { get; set; }
  public decimal Amount { get; set; }
}

DataContextに追加したモデルたちもpartialなので同じように定義します。

[DataServiceKey("OrderID")]
partial class Order
{
}

[DataServiceKey("OrderID","ProductID")]
partial class Order_Detail
{
}

[DataServiceKey("ProductID")]
partial class Product
{
}

[DataServiceKey("CategoryID")]
partial class Category
{
}

これをプロキシとなるDataModelsにまるっといれこんで以下のようなクラスができました。

public class DataModels
{
  public DataL2SDataContext Context { get; private set; }

  public DataModels()
  {
    Context = new DataL2SDataContext();
  }

  public IQueryable<Category> Categories
  {
    get { return Context.Categories; }
  }

  public IQueryable<Product> Products
  {
    get { return Context.Products; }
  }

  public IQueryable<Order> Orders
  {
    get { return Context.Orders; }
  }

  public IQueryable<Order_Detail> OrderDetails
  {
    get { return Context.Order_Details; }
  }

  public IQueryable<DailyOrderSummary> DailyOrderSummaries
  {
    get
    {
      return 省略
    }
  }
}

なんか無駄にコードの量が増えてきて面倒になってきた...。

astoria7 astoria8 astoria9

ナイス!

ここまで来たら、VS2010でADO.NET POCO Entity Generatorを使って同じようにやりたくなるってもんですよね。

ADO.NET C# POCO Entity Generator

このジェネレータはT4になってて、EDMXファイルを指定すると、ObjectContex派生のコンテキストクラスと、エンティティPOCOクラスを生成してくれます。

もちろんPOCOエンティティなのでDataServiceKey属性定義は必要だね。やってみよう!

astoria10 astoria11

おぉ~、いいじゃ~ん。と思ったのもつかの間。

astoria12

ジェネレートしたエンティティを参照すると↑こんなの出てきてちゃんと動かない。なんでじゃ~。出力されたエラーを見てみると...。

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed xml:base="http://localhost:29890/Data.svc/" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
  <title type="text">Categories</title>
  <id>http://localhost:29890/Data.svc/Categories</id>
  <updated>2010-07-25T06:29:58Z</updated>
  <link rel="self" title="Categories" href="Categories" />
  <m:error>
    <m:code></m:code>
    <m:message xml:lang="ja-JP">内部サーバー エラーです。型 'System.Data.Entity.DynamicProxies.Categories_549968D49334B0D9E524C2FC37C19156947873B86A46A78227AFE184CB25AA68' は複合型またはエンティティ型ではありません。</m:message>
  </m:error>

なんじゃこれ。いろいろ調べてみたらDynamicProxiesを使わないようにすればいいという記述を発見。

ADO.NET C# POCO Entity Generator and Data Services
ObjectContextOptions プロパティ (System.Data.Objects)

というわけで、POCO GeneratorのTTファイルをいじることにしてみます。

VSに生成されてる~.Context.ttファイルを開いて、Constructorsリージョンで隠れてる部分を開いて以下のように変更。

#region Constructors

public <#=code.Escape(container)#>()
  : base(ConnectionString, ContainerName)
{
  ContextOptions.ProxyCreationEnabled = false; 
<#
WriteLazyLoadingEnabled(container);
#>
}

public <#=code.Escape(container)#>(string connectionString)
: base(connectionString, ContainerName)
{
  ContextOptions.ProxyCreationEnabled = false; 
<#
WriteLazyLoadingEnabled(container);
#>
}

public <#=code.Escape(container)#>(EntityConnection connection)
: base(connection, ContainerName)
{
  ContextOptions.ProxyCreationEnabled = false; 
<#
WriteLazyLoadingEnabled(container);
#>
}

#endregion

この状態で動かしてみます。

astoria13

今度は通常のエンティティクラスの参照もうまくいきました。ちなみにttファイルをいじってしまえば、DataServiceKey属性もハナからくっついた状態でエンティティクラスを生成させることも出来ますね。

using System.Data.Services.Common;
<#
    BeginNamespace(namespaceName, code);
    bool entityHasNullableFKs = 
       entity.NavigationProperties
             .Any(np => np.GetDependentProperties()
                          .Any(p=>ef.IsNullable(p)));
#>
[DataServiceKey(<#= string.Join(",",entity.KeyMembers.Select(km=>"\""+km.Name+"\""))#>)]
<#=Accessibility.ForType(entity)#> 
 <#=code.SpaceAfter(code.AbstractOption(entity))#>
 partial class <#=code.Escape(entity)#>
 <#=code.StringBefore(" : ", code.Escape(entity.BaseType))#>
{
<#
    region.Begin("Primitive Properties");

    foreach (EdmProperty edmProperty in 
                           entity.Properties
                                 .Where(p => p.TypeUsage.EdmType is PrimitiveType && 
                                             p.DeclaringType == entity))

太字の1行。

ようはPOCOのみで構成してしまえば、Read OnlyのData Servicesは比較的簡単に公開できるね、っていう話。

なんだけど、本質的にはData ServiceのレイヤでごちゃごちゃせずにDBにViewを定義して、そのViewをそのまま公開するのが本筋だと思うわけです。Viewが集計に必要なRaw状態になってれば、あとはPivotTableなり使ってExcelでよしなにやってしまえよ、というのがセルフBIのスタートラインなんじゃないのかね。賢くないことをしようとすると無駄なことをたくさんしないといけなくなるから、そこんとこちゃんとやりたまえよ。

2010年7月18日日曜日

Programming Microsoft ASP.NET MVC

Amazon.co.jp: Programming Microsoft ASP.NET MVC: Dino Esposito: 洋書

先月6/3にこの本が届いてから、毎日通勤電車で少しずつだけど読み進め、やっと終わることが出来たっす。時間かかり過ぎ。

ASP.NET MVC 2に関する書籍は発売延期を繰り返しまだ全然出てきてないですが、7/21にはAmazon.co.jp: Pro ASP.NET MVC 2 Framework, Second Edition: Steven Sanderson: 洋書も届くので、その前に読み終えることができてちょっとホッとしてます。

3部構成になっていて、1部はMVCとはなんぞや、2部は内部構造、3部は機能紹介と結構豊富でAppendix含め全549ページ。読み応え抜群です。

1部のMVC、MVP、MVVPのアーキテクチャデザインの解説はかなり勉強になります。Original MVCとModel 2のMVCの違い、ASP.NET MVCはModel2なのでレイヤ的にModelはView Modelなんだよ、だったり(ヘルプ - IBM WebSphere Help Systemを見てもわかるとおりMVC Model 2でのModelとビジネスロジック+DALは別物)。Chapter 3は何度読んでも面白いです。それ以外のASP.NETとIISの関係なんかは知ってる人には面白味にかける部分かも。2部以降少し新鮮味に欠ける部分も多くなりますが、明確に書かれているのを見たことがないMVC1とMVC2の機能の違い(全然知らなかったのがTempDataの変化)がところどころ差し込まれてるので、これまた面白いです。3部のChapter 11はカスタマイズポイントが事細かに書かれているので、個人的には一番盛り上がりました。ValueProviderはあんまり取り上げられてなくてちょっと残念。というか、Providerに関してはそれほどかも。

Chapter2のNote枠に書かれてたことで、なぬ~と思ったところがあったので紹介しておくと「.NET4では直ってるけど、system.WebServer/handlersにUrlRouting.axdが無いとIIS7の統合モードで動かないバグがあるよ」っていうところ。ローカルIISにデプロイして確認してみたところ、404エラーが出てちゃんと機能させることが出来ませんでした。気をつけようね!

なんだかんだと、面白いことがたくさん書かれてて、読んで損することは絶対ないと言い切れる内容だと思います。英語でDinoさんの難しい言い回しの部分は多々あるけど、その辺は適当に辞書引いて関係ないと思ったら読み飛ばし、気になるところだけがっつり読み込む感じで十分楽しめます!

次の本は776ページらしいので、またのんびり2カ月くらいかけて読んみようと思います。

鼻唄三丁と401

401と言えばUnauthorizedなんて言う人はHTTP好きすぎ。今回は違ってAppleからリリースされたiOS4.0.1→401です。

アップル - iPhone - 新しいiOS 4ソフトウェアアップデートの機能をご紹介します。

昨日の夜に早速適用したら、それはそれは残念な結果に。なんとまぁ更新に失敗してしまいまして。原因はよくわからないけど、何度やっても途中で失敗する。ダウンロードファイル(iPhone3,1_4.0.1_8A306_Restore.ipsw)を消してやり直してもダメで、全く起動できなくなりました。泣ける。過去一度もこんなことになったことないのに~。

しょうがないから場所もよく覚えてない(一度行ったことあるはずなんだけど)銀座アップルストアに行ってきたですよ。そしたら、その場で更新適用したらすんなり出来たみたいで「普通にできましたよ~」って言われた。ギャフン。ちょっとキレ気味で「どうなんてんのさ、電話もできねーじゃん!」って文句言ってごめんね。帰ってから同期したら普通に元通りでホッとしました。

IMG_0002 IMG_0001

ちなみに写真は電車で銀座に行くのにつかった京急の八丁畷。のどかすぎです(ホネだけに)。

Bing翻訳(Bing Translator)のブックマークレット

違うウィンドウ(タブ)で開くとき。

javascript:window.open('http://www.microsofttranslator.com/bv.aspx?ref=Internal&from=&to=ja&a='+encodeURI(location.href),'_blank','');void(0);

Bing翻訳

同じウィンドウで翻訳するとき。

javascript:location.href='http://www.microsofttranslator.com/bv.aspx?ref=Internal&from=&to=ja&a='+encodeURI(location.href);

Bing翻訳

とりあえず、見つからなかったので。

2010年7月11日日曜日

GridViewも使ってます

MVCだけじゃないよ。WebFormも使ってるよ。最近GridViewで表示結果が1件だけだったら最初から選択状態にしたいな、なんて思ったわけですが、そんなときってSelectedIndexに行番号セットすれば済むよね。

  <asp:GridView runat="server" ID="gridView" 
    DataSourceID="dataSource" AutoGenerateColumns="false" 
SelectedRowStyle-CssClass="selectedRow" ShowHeader="false" DataKeyNames="Name"> <Columns> <asp:CommandField ShowSelectButton="true" /> <asp:BoundField DataField="Name" /> <asp:BoundField DataField="AkumaNoMi" /> </Columns> </asp:GridView> <asp:ObjectDataSource runat="server" ID="dataSource"
TypeName="WebApplication1.People" SelectMethod="Select">
</asp:ObjectDataSource> <asp:Button runat="server" ID="button" Text="ぼたーん" /> <h2 runat="server" id="subject"></h2>

こんなページでしょうか。

gridview1

コードビハインドはこうですよね。

  public class Person
  {
    public string Name { get; set; }
    public string AkumaNoMi { get; set; }
  }

  public class People
  {
    private static List<Person> _people = new List<Person>
               {
                 new Person {Name = "ルフィー", AkumaNoMi = "ゴムゴム"},
                 new Person {Name = "ニューゲート", AkumaNoMi = "グラグラ"},
               };

    public IEnumerable<Person> Select()
    {
      return _people;
    }
  }

  public partial class _Default : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      gridView.SelectedIndexChanged += (s, ev) => 
subject.InnerText = gridView.SelectedIndex + "行目"; } }

ObjectDataSourceを使ってるのはデータバインドの処理とか実際にはコード書かないっていう前提だからです。この状態で、選択ボタンを押すとちゃんと「n行目」って選択した行が表示されましょう。そうでしょう。

gridview2

これって普通なんだけど、GridViewのSelectedIndexにセットするようPage_Loadに書いてみても「n行目」とは表示されませぬ。されませぬ。

    protected void Page_Load(object sender, EventArgs e)
    {
      gridView.SelectedIndexChanged += (s, ev) => 
subject.InnerText = gridView.SelectedIndex + "行目"; if(!IsPostBack) { gridView.SelectedIndex = 1; } }

gridview3


↑ちゃんとニューゲート選択されてるけど「1行目」とは表示されない。なぜでしょうか。SelectedIndexプロパティに値をセットしても、GridViewがSelectedIndexChanging/SelectedIndexChangedイベントをRaiseしてくれないからですね。これをRaiseしてあげればいいんだけどそんなことしてくれないと思うので、そんな時にはGridViewにあるprotectedなOnSelectedIndexChanging/OnSelectedIndexChangedを呼べばいいでしょう。GetMethodで取り出せばprotectedだとしても知ったこっちゃないしね。

 

    private void GridViewSelectedIndex(GridView grid, int index)
    {
      grid.SelectedIndex = index;
      var selectedIndexChanged = typeof(GridView).
GetMethod("OnSelectedIndexChanged",
BindingFlags.NonPublic |
BindingFlags.Instance); if (selectedIndexChanged != null) { selectedIndexChanged.Invoke(grid,
new object[] { new EventArgs() }); } }

これをPage_Loadの”gridView.SelectedIndex = 1;”の部分で呼び出してあげるように書き換えちゃえば、ほら出来た!

gridview4

うぬ。悪くない。でも、これと同じことをクライアントサイドで選択ボタンを押したことにすることでも、実現できますね。例えばこう。

  <script type="text/javascript">
    <% if(!IsPostBack){ %>
    $(function () { 
      __doPostBack("<%=gridView.ClientID.Replace("_","$") %>","Select$1");
    });
    <%} %>
  </script>
  

固定でニューゲートを選択させてます。Select$1の部分。これはちょっと気持ち悪いかも。悪くないとは言いがたい。UpdatePanelに入れておけばそれなりな感じでいいかもしれないけど、普通に1回PostBackされるから悩ましい。

サーバーサイドとクライアントサイドのどちらをチョイスしてもいいかなと思うけど、どうでしょうね。いきなりGridViewの話をしているのはフリで、ここからが本題です。

AjaxControlToolkit(ACT)をがんばって使ってます。WebFormでの簡単Ajax導入に関してACTはとてもいい選択肢ではありますね。お金払わなくていいし、クライアントサイドの知識も要らないし。でも気に入らない部分も多くてウキャ~!ってなる。ACTとはいえ、レンダリングされてしまえばただのHTMLとJavaScriptなわけじゃないですか。気に入らなければ書き換えちゃえばいい。どこを書き換えるかがポイントだと思いますが、今回Accordionを書き換えてみました。ボス、ごめんね!

先程のGridViewのサンプルの続きに書き足していきます。

まずはASPXに以下を追加。これでAccordionが使えます(なんつーかツールボックスからのドラッグアンドドロップのやり方を知ってるともっとラクにかけるんだろうけど、そんなやり方はわすれたので普通に手で書いてます)。

  <asp:ToolkitScriptManager runat="server"></asp:ToolkitScriptManager>
  <asp:Accordion runat="server" ID="accordion"
       HeaderCssClass="acd_header" HeaderSelectedCssClass="acd_selected" 
ContentCssClass="acd_content"> <Panes> <asp:AccordionPane runat="server" ID="pane1"> <Header>pane 1</Header> <Content>パネルのコンテンツ その1 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> <asp:AccordionPane runat="server" ID="pane2"> <Header>pane 2</Header> <Content>パネルのコンテンツ その2 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> <asp:AccordionPane runat="server" ID="pane3"> <Header>pane 3</Header> <Content>パネルのコンテンツ その3 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> <asp:AccordionPane runat="server" ID="pane4"> <Header>pane 4</Header> <Content>パネルのコンテンツ その4 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> <asp:AccordionPane runat="server" ID="pane5"> <Header>pane 5</Header> <Content>パネルのコンテンツ その5 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> </Panes> </asp:Accordion>

acd1

恐ろしいほど簡単。もちろん、サーバーサイドで動的にPaneを追加するような使い方も多いでしょう。そうすると、Paneが10個や20個にナッチャウことも。そうなるとインターフェースとしてちょっと問題あり。そんなに追加しなきゃAccordionっていいのにとつくづく思う。でも、かなり開発が進んでしまうと、おいそれとインターフェースを変更するのはしんどい作業になります。変更箇所多すぎるし。そんなこんなで、クライアントサイドで何とかしてしまいます。

acd2acd3

完成形は↑これです。何をしてるのかというと、AccordionのHeaderをSelect(DropDown)にして、選択したら該当するPaneの中身を表示するという動きです。ヒマがあったら動かしてみてね。

まずは、AccordionがレンダリングするHTMLを確認。

<div>
  <input type=”hidden” value=”選択してるPaneのIndex”/>
  <div class=”header class name”>header 1</div>
  <div class=”content class name”>content 1</div>
  <div class=”header class name”>header 2</div>
  <div class=”content class name”>content 2</div>
</div>

こんな構造ですね。ヘッダとコンテンツが、連続してるdiv要素をクラス名で判別というのがちょっと気に入らないですが、今回そこはどうでもいいです。最初にこういう構造でレンダリングされるというのがわかりさえすればいいです。

続いて、どういうスクリプトが動いてるのかを確認してみると、ページ内では↓これだけですね。

<script type="text/javascript"> 
//<![CDATA[
(function() {var fn = function() {
$get("ctl00_MainContent_ToolkitScriptManager1_HiddenField").value = '';
Sys.Application.remove_init(fn);};Sys.Application.add_init(fn);})();


Sys.Application.add_init(function() { $create(Sys.Extended.UI.AccordionBehavior,
{"ClientStateFieldID":"ctl00_MainContent_accordion_AccordionExtender_ClientState",
"HeaderCssClass":"acd_header",
"HeaderSelectedCssClass":
"acd_selected","id":"ctl00_MainContent_accordion_AccordionExtender"},
null, null, $get("ctl00_MainContent_accordion")); }); //]]> </script>

Sys.Application.add_initのタイミングでAccordionBehaviorオブジェクト(クラスのような作りかな?)を渡してますね。そもそもSystem.Extendedが何者なのかしらないですけど、ソースが公開されてるので確認してみたところ、各種Behaviorオブジェクトのprototype.initializeでいろいろ初期化処理を書いてました。

ということは、そこを壊せば、Accordionの初期化処理は走らなくなるということなので、add_initで追加したものが実行される前に、AccodionBehavior.prototype.initializeを壊してみました。runat="server"なform要素の閉じタグ直前に以下のコードを書き足します。

  <script type="text/javascript">
    if (Sys.Extended !== undefined && 
Sys.Extended.UI.AccordionBehavior !== undefined) { Sys.Extended.UI.AccordionBehavior.prototype.initialize = function () { }; } </script>

やりすぎな感じもしますが、まぁいいでしょう。

続いてドロップダウンを表示する枠と、Paneの中身を表示する枠を用意します。Accordionタグの上にでもおいておきます。

  <div id="dropdown"></div>
  <div id="viewer"></div>

最後に、Accordionそのものが表示されてしまわないように、Accordionを非表示するためのCSSを定義します。Visibleを指定しちゃうとホントになくなっちゃうからCSSだけで消しちゃうのがいいです。

  <asp:Accordion runat="server" ID="accordion"
       CssClass="accordion_hack" HeaderCssClass="acd_header" HeaderSelectedCssClass="acd_selected" 
       ContentCssClass="acd_content">

太字のところで、クラス名指定して、そのクラスはdisplay:none;だけを適用してます。

あとはJavaScriptでDropDownを生成するのと選択肢を変えたときに表示を切り替えるようなコードをゴリゴリ書けば完成。

<script type="text/javascript">
  $(function () {
    var hack = function () {
      var accordion = $(".accordion_hack:first");
      var viewer = $("#viewer");

      var dropdown = $("<select />");
      var currentId = accordion.find(".acd_selected")
.next()
.attr("id"); var prevId = currentId; accordion.find(".acd_content").each(function (idx, e) { $(dropdown).append( $("<option />").text($(e).prev().text()) .val($(e).attr("id"))); }); $(dropdown).appendTo("#dropdown"); $(dropdown).change(function (e) { prevId = currentId; currentId = $(this).val(); viewer.fadeOut("fast",function () { if (currentId != prevId) { $("#" + prevId).appendTo(accordion); } $("#" + currentId).show().appendTo(viewer); }).fadeIn("fast"); // accordion.find("input[type='hidden']:first")
.val($(this).find("[value=" + currentId + "]").index()); }); $(dropdown).val(currentId).trigger("change"); }; hack(); }); </script>

わざわざhack変数にいれてるのは、特に意味ないです。テスト目的なので気にしないでね!長々とMVCとは関係ないことを書きましたが、要はJavaScriptとHTML、DOM操作、CSSを理解してると、ASP.NETもいろいろカスタムなレンダリングを気軽に実装できるよっていう話なのと、ACTをハックすることでちょっと勝てた気になれたので、調子にのってエントリしてみました。

今回の一式もダウンロード出来るようにしておきます。興味があればどうぞ。

だた、注意点があります。VS2010でASP.NET 4をターゲットにして作ってるのに、ACTは4.0じゃなくて3.5向けのものを使ってます。理由はACT4のAccordionPaneがMasterPageを使ってる時に、id属性を正しく吐き出してくれないからです。3.5だと問題ないので、4.0でなんかおかしくなったんじゃないかな。気をつけてね。

2010年7月6日火曜日

DeserializingModelBinder

モダンチョキチョキズを知ってるのはどのくらい少数派なのかが気になる今日この頃。

ASP.NET MVC 2 Futuresに含まれているHtml.SerializeとDeserializeAttributeをクラスのプロパティに対して適用するのにお悩みな方へ。DeserializeModelBinder自体がなぜかDeserializeAttributeクラスのprivate sealedクラスと定義されてしまっているので、全く同じものを以下のように定義することで、ModelBinderAttributeを指定して比較的簡単に出来るようです。

  public class DeserializingModelBinder : IModelBinder
  {

    private readonly SerializationMode _mode;

    public DeserializingModelBinder() : this(SerializationMode.Plaintext) { }
    public DeserializingModelBinder(SerializationMode mode)
    {
      _mode = mode;
    }

    public object BindModel(ControllerContext controllerContext, 
ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException("bindingContext"); } var vpResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName); if (vpResult == null) { // nothing found return null; } MvcSerializer serializer = new MvcSerializer(); string serializedValue = (string)vpResult.ConvertTo(typeof(string)); return serializer.Deserialize(serializedValue, _mode); } }

↑これはソースからコピペです。MvcSerializerが使えなかったらどうしようかと思いましたが、こちらは平気ですね。

  public class Division
  {
    public string Name { get; set; }
    public Person Boss { get; set; }
    public People People { get; set; }
  }

  [Serializable]
  [ModelBinder(typeof(DeserializingModelBinder))]
  public class People : List<Person>
  {}

  [Serializable]
  public class Person
  {
    public string Name { get; set; }
    public DateTime MemorialDay { get; set; }
  }

↑このようなモデルクラスたちを定義してみました。あえてPeopleクラスを定義しているのはModelBinderAttributeがプロパティに指定できないからです。DeserializeAttributeにいたってはParameterにしか指定できないし、ModelMetadataProviderらへンに手を入れる必要がある気がしなくもなく(たぶんメタデータを見てModelBinderを切り替えるような仕組みでしょうか)、難しそうだったので使っていません。

このようにクラスを定義した上で、ModelBinderAttributeでModelBinderを指定する方法が比較的簡単でスマート(?)じゃないかと思います。こうしておくとアクションでは何も意識する必要なく以下のようにアクションパラメータを生成してくれるようになります。

  public ActionResult Division()
  {
    var model = new Division
    {
      Name = "ブチャラティチーム",
      Boss = new Person { Name = "ブローノ・ブチャラティ", 
MemorialDay = new DateTime(2010, 1, 1) }, People = new People { new Person {Name = "ジョルノ・ジョバァーナ",
MemorialDay = new DateTime(2010, 2, 1)}, new Person {Name = "レオーネ・アバッキオ",
MemorialDay = new DateTime(2010, 3, 1)}, new Person {Name = "グイード・ミスタ",
MemorialDay = new DateTime(2010, 4, 1)}, new Person {Name = "ナランチャ・ギルガ",
MemorialDay = new DateTime(2010, 5, 1)}, new Person {Name = "パンナコッタ・フーゴ",
MemorialDay = new DateTime(2010, 6, 1)} } }; return View(model); } [HttpPost] public ActionResult Division(Division model) { return View(model); }

Viewは以下のようにシンプルです。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Division>" %>
<%@ Import Namespace="Microsoft.Web.Mvc" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
  Division
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Division</h2>

  <% using(Html.BeginForm("Division","Home")) { %>
    <%= Html.EditorFor(m=>m.Name) %>
    <h3>Boss</h3>
    <%= Html.EditorFor(m=>m.Boss) %>

    <h3>People</h3>
    <% foreach (var person in Model.People) { %>
      <%= Html.DisplayFor(m=>person) %>
    <% } %>

    <%= Html.Serialize("People",Model.People) %>
    <input type="submit" value="送信" />
  <% } %>
</asp:Content>

これを実行するとこのようにきちんと復元してくれます。

serialize1 serialize2 serialize3

@jsakamotoさん、いかがでしょうか?

2010年7月4日日曜日

Razorのポテンシャル

Introducing “Razor” – a new view engine for ASP.NET - ScottGu's Blog

↑これ読みました?

Razor View Syntax

↑これも。

これね、よくよく考えたらかなり凄いことに取り組んでるんだと思います。そもそもASP.NET MVCはASP.NETのフレームワークにのっかったものですよね。それはどういう事かというと

  • HttpApplicationのパイプラインで処理
  • HttpContextで現在のリクエストコンテキストに関する全ての情報にアクセス可能
  • System.Web.UI.PageがIHttpHandlerの実装としてPageパイプラインを処理しつつレンダリング

ですね。ASP.NET MVCのViewでは、このフレームワーク(Pageに関する部分)の使い方で変わった部分はほとんどないんですが、構造上使われなくなったものといえばコントロールツリーの構築とポストバックです。コントロールツリーに関しては「runat="server"なコントロールを使ってる場合には作られるじゃないか」と言われればそうなんですが、そこはPageクラスがそうだからで、Viewの性質として必須な機能じゃないのでWebFormとの対比という意味では使ってないと言うことにしましょう。ポストバックですが、これは特にMVC2になってから大きく変化のある部分で例えば以下の単純なViewでもIsPostBackはtrueになりません。

<%@ Page Language="C#" 
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %> <script runat="server"> void Page_Load() { if(IsPostBack) { // PostBack eventはいずこ?
// ここには決して入ってこない } } </script> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <form runat="server"> <asp:Login ID="Login1" runat="server"> </asp:Login> </form> </asp:Content>

Viewのレンダリングの仕様が変更になってるのでWebFormViewにはイベントがイベントとして認識できない形になったから(TextWriter渡しのRender呼び出し)。ようはControllerに対するPOST/PUT/DELETE/GETのHttp MethodがViewに影響を及ぼすことを是としない、という意志の表れなんじゃないか~、なんてな。

話がずれましたがRazorはこのPageクラスに依存しないViewEngineとして造られてるので、Page Pipeline(イベント)は処理されない、コントロールツリーも構築されないという事になって、ASPX構文解析→C#生成の部分と、Razorの準備コスト(テンプレートエンジンとしてのコスト)が同程度だとしても、処理コストは低くなるのでよりスケールするフレームワークになるんじゃないかと思います。

Pageクラス(WebForms)におけるPostBackという発明はPage Controllerの素晴らしい実装だと思いますが、MVCとなればControllerは役割の違うものとして分離されてるので、PostBackは不要だし、それを実装してるPageクラスはリッチすぎるでしょう。DataSetからRepositoryへのスタイルの変化も同じような考え方からきてるんじゃないでしょうかね(メモリバウンドよりCPUバウンド的な)。Pageクラス、Page Pipelineからの脱却。凄くないですか?

と、なんの根拠もないですが、妄想したりしてます。

2010年7月3日土曜日

ふぉー!フォー!デレシシシ。

なんかね~、そっけない箱がね~、あったんですよ。

IMG_0005


でも、開けるとピカピカな箱なんですよ。

IMG_0006


長かった。オンラインショップで予約した負け組で始まった今回の機種変。サイトダウンにめげずF5アタックを繰り返すも当日予約は途中で断念。翌朝7:30にはすんなり予約できたから、これは当日入手期待大だと妄想。それまでiPhone 3GにiOS4を入れて気分だけでも味わってやろうとしたら信じられないくらいパフォーマンス悪くてなんどもたたき壊してやろうかと思う辛い日々。だけど発売日を過ぎてもなんの連絡も無く。ちなみに受付番号下6桁は818XXX。一体全体この放置プレーはなんだ。そんなこんなで6/30にメールが来た。遅すぎるし放置しすぎ。遅れております、のメール1つで全然違うのになぜ放置。まぁ、いいか。

で、箱。長かった。

さて、アクティベーションしようかとしたところ、9:00~19:00が受付時間らしく何も出来ず。敗北感。仕事変えようかと本気で思った。

IMG_0009

もろもろ設定完了して使ってみると3Gとは比べものにならないくらいのパフォーマンスで操作が快適。さすがですぜ!

2010年6月22日火曜日

千の夜をこえて

いやはや、以前100を超えた時に次は500を目標にする!と言ってからほぼ1年?

stackoverflow

長かった。やっと500を超えた(いつマイナス評価がついて500を切るかヒヤヒヤするけど)デス!

嬉し~よ~。

んで、仕事で面接したりしてるんだけど、興味がある方は採用情報 | 株式会社クロスワープここから応募してみてね!意地悪なこと聞かないよ~。コーディングのテストはあるよ~。また社員募集やってますと同じ募集要項ですよ~。テスト問題はたけはらが考えましたよ~。なので「リンクリストの循環参照検出」なんていう難しい問題ではないですよ~。ぜひどうぞ~。

2010年6月18日金曜日

yield break

最近HaackさんとこでChecking For Empty EnumerationsNull Or Empty CoalescingでEmptyなIEnumerableに対する拡張メソッドで盛り上がりを見せてるますね。面白いですよね。

でね、ふと思ったんですけどね、IEnumerable<T>の空イテレータってどう表現するのがオシャレなのかな、と。

  public IEnumerable<T> Empty<T>()
  {
    return Enumerable.Empty<T>();
  }

  public IEnumerable<T> EmptyZero<T>()
  {
    return new T[0];
  }

  public IEnumerable<T> EmptyYield<T>()
  {
    yield break;
  }

んん~。個人的にはyield breakなんだけど。

2010年5月29日土曜日

ラテアート

いったいいつまでブログ放置をし続ければ気が済むのかね。ズボラにも程がありますよ、まったく。かといってコレといって面白いことも無く。あ、最近Code Contractsが面白そうか。

と、いいつつも最近テニス始めたんですよ。

IMG_2552

最近と言っても、今年に入ってからなんでもう半年位たちますね。いやはや。でも月1くらいしか出来てないんだけど、やり始めるとテレビとかでもテニスみちゃうもんですね。フランスのレザイが要注目やで!

雨の日にランチを食べに行くカフェでたまに素敵なカフェアートをしてくれるんですけど、これがちょっとカワイイ。

IMG_2568

いきなり3Dですよ!アリス イン ワンダーランドも真っ青ですよ。

IMG_2580

4匹のクマちゃん?たぶんクマちゃん。

IMG_2586

で、iPad。あ、間違えちゃった。ラテアートと間違えちゃった。てへ。

なんせ本を読むのとビデオを見るのをメインの用途に考えてるんだけど、本ってどうやったら見れるんだろ。Kindle入れとけばいいのけ?それともCloudReader?

ってことで、↑の写真はCloudReader入れてるところ。KindleってAmazon.comのアカウントじゃないとダメなんすかね。登録してとりあえず無料の本を買ってみたけど...。

もっと気軽に普通にPDFをみるのもいいかと思うも、そもそもPDFをどうやってiPadにコピーしたらいいのかよくわかんないじゃないか。Dropboxが入れてあるからそっちからPDF開いてみたら普通に見れたけど、なんかそんな面倒なハズない!と調べてみたらなんてことなかった。iTunesからD&Dで転送出来た。まずはPDF(いろんな公開ドキュメント)が見れるようになるだけでも超便利。iPhoneで見るとか小さすぎてありえない。見れるのと、ちゃんと読めるっていうのとこんなにも違うのかと電子書籍の衝撃ですよ(この本とても面白いのでおすすめ。あとアーキテクトの審美眼も面白かった)。

来週からの通勤が楽しみです。

そうそうそうそう。社員募集してるみたいデス。興味があればどうぞ。

2010年2月28日日曜日

続Demotterについて

途中まで書いて力尽きたので、続きを。

ValuProviderFactoryについての説明まで進みましたが、そもそものIValueProviderは何なのかというところをすっ飛ばしてます。IValueProvider自体はMVC 1の頃から存在してるものです。GetValueでValueProviderResultとして値を取得するためのインターフェースです。UpdateModelなんかもIValueProviderを渡すオーバーライドがちゃんとあります。FormCollection.ToValueProvider()なんかを渡しますね。

ようは何でもいいからキーに対応する値を渡せばいいんです。基本実装のDictionaryValueProviderは値の実体そのものをキーと共に保持してるものなんですが、キーに対応する値を常に保持しておく必要はなく、GetValueのたびに取得しにいくという実装もアリです。HttpCookieValueProviderの例としてとてもいいサイトが有ります。

Dive Deep Into MVC - IValueProvider - Mehdi Golchin's blog

このサンプルコードではGetValueのたびにRequest.Cookieを参照する作りなのは、値の実体はHttpContextにそもそも保持されてるので、Dictionaryとして二重に保持する必要がないからです。ただ、これだとテストはHttpContextBaseのモック作成から必要になるんで、少し面倒ですけど。

Demotter(MvcPresetner)で作成したのは、PresentationZipValueProviderFactoryでプロジェクトルートにあるやつです。

mvcp

これまで説明したValueProviderFactoryのサンプル実装としてZipファイルをアップロードすると、サーバーサイドで解凍し、Presentationクラス(特にModelBinderの対象となるクラスを限定する縛りはないです)のインスタンスを生成するためにDefaultModelBinderから利用出来るようになっています。

なんで、ここに注目してるのかというと、SRP(Single Responsibility Principal)でテストを簡単にしたいからです。やっぱり楽して作りたいというのがあるからね。Actionのテストってモデルクラスを渡してしまえば、実行コンテキストに依存させなくてもいいじゃないです。DefaultModelBinderは標準機能だからテストなんかしなくていい。そうすると、カスタムValueProviderFactoryだけがHttpContextBaseのMockを使ったテスト対象になるわけです(ViewのテストはWebサーバーで実際に動かしてSeleniumとかでどうぞ)。素晴らしいリファクタリングだと思います。

Zipファイルを解凍するために、外部の依存アセンブリとしてDotNetZip Libraryを利用しています。ZipFileクラスのReadでZipファイルを指定し、ExtractAllで全解凍です。使ってる機能はそれだけ。

PresentationのリポジトリとしてStoragePresentationRepositoryクラスを作ってます。IPresentationRepositoryのファイルストレージ保存用の実装です。なので、ここはデータベースに保存するようなRepositoryを実装すれば、データファイルの保存場所は上位のサービス層(このサンプルではサービス層はなくControllerで直接Repositoryを使ってます)が知る必要はないような作りです。

実ファイルを”~/App_Data”に保存するようにしてるのでServer.MapPathを使う必要があり、Controller.InitializeでIPresentationRepositoryの実装クラスのインスタンスを作成してるので、LinqToSqlPresentationRepositoryを作成したとしても、単純変更はできないのが、手を抜いたところです。RepositoryのResolverというかCreatorを作っておいて、そいつに任せるようにしておく実装であれば依存性を排除できますね。

    private IPresentationRepository _repository;
    public static Func<RequestContext,IPresentationRepository> RepositoryCreator = 
      (requestContext) => new StoragePresentationRepository(
requestContext.HttpContext.Server.MapPath("~/App_Data")
); protected override void Initialize(RequestContext requestContext) { base.Initialize(requestContext); // Serverプロパティなどの参照はInitialize以降じゃないと // できないので気をつけましょう _repository = RepositoryCreator(requestContext); }

たとえば、現状のコードを↑こうしてみるとか。LinqToSqlPresentationRepositoryはRequestContextを必要としないですけど、簡単にするにはこういうのを渡すルールにしておくのもいいんじゃないですかね。Global.asaxなんかで以下のようにCreatorを変えちゃえば、うまくいくはず。

    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();

      RegisterRoutes(RouteTable.Routes);

      ValueProviderFactories.Factories.Add(new PresentationZipValueProviderFactory());

      HomeController.RepositoryCreator = (_) => new LinqToSqlPresentationRepository();

      // カスタムModelBinderを使うなら↓ここで登録忘れずに。
      //ModelBinders.Binders.Add(typeof(Presentation), new PresentationModelBinder());
    }

ここはMVC関係ないところ。今回のサンプルではControllerが生成されるたびに、毎回Repositoryの中でApp_Dataを見てPresentationのインスタンスを取得するので無駄が多いですが、その辺もサンプルということで勘弁してもらえると助かります。

ModelBinderは標準のDefaultModelBinderを使っていて、DataAnnotationModelValidatorがそのまま機能します。カスタムModelBinderでもDataAnnotationを機能させるなら、多分以下のような作り方になると思います。

  /// ValueProviderFactoryを定義しない従来の手法だと、ModelBinderを作成して
  /// 以下のように自分でマップしたモデルに対してValidationを実行することになります。
  public class PresentationModelBinder : DefaultModelBinder
  {
    public override object BindModel(ControllerContext controllerContext, 
ModelBindingContext bindingContext) { // ここでモデルを生成してModelMetadataに入れておくと、 // CreateModelでは生成せずに、OnModelUpdating/OnModelUpdated // を内部で呼び出してくれるようになる。 // でも、この書き方であってるのか自信無いですが...。 var valueResult = bindingContext.ValueProvider.GetValue("Name"); var model = StorageAccessor.Load(
PresentationZipValueProviderFactory.UploadTempPath,
true, valueResult.AttemptedValue); bindingContext.ModelMetadata.Model = model; return base.BindModel(controllerContext, bindingContext); } }

これがあってるのかどうかは自信がないです。BindModelの戻り値にインスタンスをそのまま返すだけではDataAnnotationが効かないので、BindingContext.ModelMetadata.Modelに対象モデルのインスタンスを入れて、後はbase.BindModelに任せてしまう実装です。いいのかな~。ちゃんと動くのは確認してます。

リポジトリから取得できたものをHTMLとして生成するために、Viewにモデルを渡し(Presentationクラスのインスタンス)、後はViewにまるなげです。

スライドとして表示したいデータをMarkdown書式で送信したものを利用するようにしてるので(Stackoverflow.comをまねっこしてみたかった)、Markdown書式からHTMLに変換する必要があります。クライアントでの変換実装としてWMDというのもありますが、今回はサーバーサイドでHTMLに変換するMarkdownSharpを利用しています。2個目の外部依存アセンブリです。Markdownで厳しいところはUL/LIの入れ子が2段までしかできないところ。できる方法があるんだろうか。当たり前ですが、利用目的がスライドじゃないのでしかたないところですね。

スライドの動き自体はカーソルの上下でフェードさせながらの切り替え、左右でアニメーション無しでの切り替えの2種類のみで、リッチなアニメーションは実装してないです。その辺はS5やS6なんかがあるので、そっちに差し替えてもらえればいいかな~、なんて。

そんなこんなで、実行すると↓こんなです。

mvcp2

後は、F11でフルスクリーン表示にしておけば、それっぽく見えます。

あとアップロードしたコンテンツの削除をHttpDelete属性を指定したActionで実装してますが、これだけだとHttpVerbs.DeleteなリクエストじゃないとActionInvokerの対象として選択されないです。一般的なブラウザではGET/POSTしか送信してくれないので困りもの。でも、MVC 2ではHttpVerbsのオーバーライドを簡単にできるように拡張メソッドも用意されてるので、HtmlHelperの拡張メソッドHttpMethodOverrideをForm内で呼び出せば、POSTでもオーバーライド(hiddenに埋め込まれる)されてうまく動くようになります。Railsなんかでも"_method"でHttpVerbsをオーバーライドできるのでそれと同じです。

Viewでは以下のように書いてます。

    <ul>
    <% foreach (var item in Model) { %>
    
    <li>
      <%= Html.RouteLink(item.Name, "Viewer", new { id = item.Name })%>
      &nbsp;-&nbsp;
      <% using (Html.BeginForm("Delete", "Home", new { id = item.Name }, 
FormMethod.Post, new { style = "display:inline;" })) { %> <%= Html.HttpMethodOverride(HttpVerbs.Delete) %> <input type="submit" value="削除" /> <% } %> </li> <% } %> </ul>

簡単ですね。

mvcp3

↑こんなボタン出てきます。なんで、わざわざHTTPメソッドでActionを選択するのかというのはRESTfulなアーキテクチャスタイルの話になるので割愛。ただ、この実装方法であれば、ブラウザ以外からDeleteやPutのリクエストと、ブラウザからの同リクエストを区別するようにActionを書かなくて済むのがいいですよね。もちろんAction名が”Remove”とか”Update”とかでPostで処理をするようにしても、結果は一緒ですけどね。

DemotterことMvcPresenterが何を実装したサンプルなのか、だいたい分かってもらえたでしょうか。これを10分で話すのはさすがに無理ですね。詰め込みすぎました。

Demotterについて

ASP.NET MVC 2になって変更された箇所はとても多いです。MVC 2での新しくなった部分を紹介するサンプルとしてMvcPresenterというのを作成することにしました。Demotterという名前はEdtterへのオマージュ(?)。

MVC 2の新機能のうちマニアにはたまらないだろうなと思って目をつけたのが各種Providerモデルへのリファクタリング部分で、MvcPresenterではそのうちIValueProviderを実装したValueProviderFactoryのカスタム化と言う部分をメインに実装しています。

いきなりそんな話をされても意味がわからないと思うので、順を追って説明していきます(ASP.NET MVCについての基本的な知識は前提です)。

そもそもMVCではポストバックがないので、TextBoxやRadioButtonなどの入力用サーバーコントロールは使用しません。HTMLとしてのinputやtextarea、selectを使用するのみです。そうすると入力値をサーバーサイドで取得するにはどうすればいいかというと以下の3通りあります。

  • Request.Formで取得
  • FormCollection型の変数をAction引数に指定する
  • ModelBinderに任せる

Request.FormとFormCollectionを使用する方法はあまりにも原始的すぎます。入力に対する検証も自分で行う必要があり、とても煩雑なコードになります。

Viewとして以下のようなものがあるとします。

  <% using (Html.BeginForm("FormPost1")) { %>
  
    <% = Html.TextBox("name") %>
    <% = Html.TextBox("age") %>
    <% = Html.CheckBox("isDeveloper") %>
    
    <input type="submit" value="do post" />
    
  <% } %>

これを受け付けるActionとしてRequest.FormやFormCollectionの場合↓こうなります。

    public ActionResult FormPost1()
    {
      var name = Request.Form["name"];
      int age;
      int.TryParse(Request.Form["age"], out age);
      bool isDeveloper;
      bool.TryParse(Request.Form["isDeveloper"], out isDeveloper);

      // 処理

      return View();
    }

    public ActionResult FormPost2(FormCollection form)
    {
      var name = form["name"];
      int age;
      int.TryParse(form["age"], out age);
      bool isDeveloper;
      bool.TryParse(form["isDeveloper"], out isDeveloper);

      // 処理

      return View();
    }

ほとんど同じなんですが、テストする際にRequestなどのコンテキストに依存させないようにするためにFormCollectionを使用するという書き方が存在します。

これに対しModelBinderを利用するスタイルの場合は以下のようになります。

    public ActionResult FormPost3(string name, int age, bool isDeveloper)
    {
      // 処理

      return View();
    }

Action引数に直接入力値が入ってきます。型変換も自動です。変換できないならエラーになるという便利なものです。でも、これだと細かく入力エラーを処理できないです。しかも値が多いとAction引数がとんでもないことになります。

なので以下のようにクラスを定義して、そのクラスのインスタンスをAction引数に取得するというスタイルがオーソドックスな手法となるはずです。

  public class Person
  {
    public string Name { get; set; }
    public int Age { get; set; }
    public bool IsDeveloper { get; set; }
  }

↑これがクラス定義で、↓これがAction。

    public ActionResult FormPost4(Person person)
    {
      // 処理

      return View();
    }

何が違うかは一目瞭然。本来personという仮引数名を使用する場合、Formのname属性にプレフィックスとして"person."とつけておくんですが、そこは自動でプロパティ名とname属性をみて一致するなら埋めてくれます。なので、あえて"person.name"や”person.age”とname属性に指定しなくてもModelBinderは賢いのでなんとかしてくれるんですね。明示的に分けたいときにname属性にプレフィックスを指定する必要があります。

クラスを指定するのも基本型を指定するのもModelBinderにしてみれば同じことです。固有のクラスを使用して、DefaultModelBinderがきちんとインスタンスを生成できないときには独自のModelBinderを作成することになると思いますが、MVC 2ではそういう手法はあまりとらないんじゃないかと思ってます。理由はValueProviderFactoryが指定できるようになったからです。

不思議に思わないですか?Routeに指定した場合でもAction引数に割り当てられるし、FormからPostしても割り当てられる。もちろんQueryStringの場合でも自動でAction引数に値がわたってくるんですよ?データの出所がそれぞれ違うじゃないですか。RouteとQueryStringはURLだから同じだと見ることもできるんですけど。

ここで、もうひとつ忘れてはいけないのがUpdateModelとTryUpdateModelです。これはIValueProviderを指定するか、Formの値を利用するかのどちらかでクラスのインスタンスを生成してくれるんですが、それもValueProviderFactoryを利用することでデータの出所を意識しなくても良くなります。

さっきから"データの出所(でどころ)"という言葉を使ってますが、それってどういう意味かというと、ModelBinderが値を復元する際にどこから値を持ってくるのか?ということです。Request.FormなのかRequest.QueryStringなのか、RouteData.Valueなのか、ですね。じゃーRequest.Cookieから復元させたいときはどうすればいいと思いますか?MVC 1の時はAction内でRequest.Cookieを直接みて自分で変数に割り当てるか、カスタムModelBinderを作成し、そこでRequest.Cookieを参照してモデルに復元させる必要がありました。MVC 2になるとデータの出所をValueProviderFactoryから取得するという仕様になっているので、カスタムなValueProviderFactoryを作成し、Global.asaxでValueProviderFactoriesに追加しておけば、標準のValueProviderFactoryで見つからなかった場合、カスタムValueProviderFactoryから値を取得して、ModelBinderが値(クラスのインスタンスか基本型)を復元してくれます。Cookieから値を取得して復元させたければ、ModelBinderを作成するのではなく、そこはDefaultModelBinderに任せたまま、CookieValueProviderFactoryを作成するとなるでしょう。そうすることでDataAnnotationも有効な状態で値を取得できます(カスタムModelBinderでもできますがそれはまた別の時に)。

ModelBinderのデータの出所(データ取得元)を自分で好きなように指定できるということです。すごいことですよね。ちなみにValueProviderFactoryとして実装しなければいけない唯一のメソッドは

public override IValueProvider GetValueProvider(ControllerContext controllerContext)

です。IValueProviderの基本実装はDictionaryValueProvider<object>で、KeyValueなディクショナリです。ModelBinderの仕組みそのものはMVC 1の時から変わってないので、詳細ははしょりますが(書いたほうがいいですか?)、キーとしてForm要素のname属性やQueryStringのKeyを指定するのを想定して値を取得します。

なのでDictionaryとしてCookieから取得した値を返そうが、JSONをデシリアライズしてキー指定で取得できるようにしたものを返そうがXMLをキー指定で取得できるようにしたとしても、Dictionaryとして取得できるならなんでもいいんです。データの出所だけではなくデータのフォーマットにも依存させなくて済むということです。

JSONとして以下のようなデータがあったとしましょう。

{
  name:'たけはら',
  age:15,
  isDevelopper:true
}

ここから以下のように値が取得できるDictionaryを返すことができれば、ModelBinderは値を復元できるということです。

dict["name"] = "たけはら";
dict["age"] = 15;
dict["isDeveloper"] = true;

おなじ理屈でXMLでもいいですよね。自分で取りやすいスキーマさえ定義しておけばいいので。つまり、ファイルシステムにKeyValueでアクセスできるValueProviderFactoryを作成するなら、Zipでアップロードしたファイルを解凍し、フォルダ構造とファイル名がキーになっていてファイルの実体が取得できるようなものも作成できるわけです。実際にファイルの実体をbyte[]なんかで復元するのはリソースの無駄遣いになるので、ModelBinderが復元するのはファイルのパスにしておくというのが現実的でしょう。MvcPresenterがまさにそのように処理をしています。

ちょっと長くて疲れてきたので、続きは今度にしてもいいですかね。いいですよね。中途半端でさーせん。眠いっす。

2010年2月24日水曜日

ダイヤモンドは砕けない

ゆりかもめに初めて乗りました。

techdays2010 

わーい、自由の女神だー。あははー。あはははー。はぁ...。

台場でTechDays2010のBoFに出てきたんです。

4 の時代の Web アプリケーションを語ってみよう

小野さんとナオキさんと三人でのおしゃべりはTechEd 2009の時とあわせて2度目です。相変わらず打ち合わせとかしないのにはヒヤヒヤです。

ASP.NET 4になっていろいろすごいじゃないですか。その辺の話をしましょうよ、ということだったので、たけはら担当はもちろんASP.NET MVC 2。一応事前に書いておいたメモをブログにも載せておきます。

メジャーな部分

  • データ検証の方法がDataAnnotationsを基本にしたものになりました。

これまで通りIDataErrorInfoを実装したものも有効ですが、ValidationAttributeをモデルまたはモデルプロパティに指定(ModelMetadataで外部に定義したものでも可能)して、DataAnnotationsModelValidatorを使用するようDefaultModelBinderが変更になっています。
LinqBinaryModelBinderも標準実装に組み込まれたので、System.Data.Linq.Binary(SqlDbType.Timestampなど)をbase64でhiddenからポストしたときも、自動で復元されます!
またLinqBinaryModelBinder、基底クラスのByteArrayModelBinder(byte[]を復元)もFuturesから昇格です。

  • ASP.NET 4で組み込まれるSystem.Web.IHtmlStringを使えるようにMvcHtmlStringクラスが導入されました。

MvcHtmlStringのソースファイルを確認するととても勉強になります。まさに黒魔術。
すべてのヘルパーはstringではなく、このMvcHtmlStringを返すよう変更されています。
<%: expression %>としている場合、自動的にHtmlEncodeした結果がレンダリングされるので、今後はこれが主流になります(きっと)。
MVC2の実装でもそうですが、2.0ベースのSystem.Webにはそんなものないので、なので、ASP.NET 4じゃない場合はこれまで通り、Html.Encode(string)を使いましょう。
IHtmlStringの場合、<%: ihtmlstring %>となっていても、HtmlEncodeをかけずに出力するので、Partial HTMLをレンダリングする場合(ヘルパーのレンダリングとかも)は、IHtmlStringとして渡しておきましょう。

  • AcceptVerbsAttributeがHttpVerbs.Get/Post以外に、Put/Delete/Headにも対応するようになっています。

Get/Post以外は通常、ブラウザからは送信されないですが、HtmlHelper.HttpMethodOverride()をformタグ内で呼んでおくことで、hiddenにX-HTTP-Method-Overrideという名前で、メソッドオーバーライドを保持するようになり、POSTを使ったHttpVerbsの上書きができるようになります。
この辺の実装はHttpRequestExtensionsに用意されてるHttpRequestBaseに拡張メソッドとして実装しているGetHttpMethodOverride()がまるっと処理してくれるようになっています。
もちろんここも、AcceptVerbsAttributeの判定メソッドがoverride可能になっているので、独自のAcceptVerbsAttributeを実装することで、Railsライクに"_method"というオーバーライドを使うように変更することも可能です!
これは今までのバージョンではとても面倒な実装 にしないとできない部分でした。
ちなみに、RESTfulなController実装をするときに、これができないと、ブラウザからのリクエストと、その他のクライアントからのリクエストの処理を簡単に切り分けできなくてとても不便です。

  • JsonResultを返すActionの場合、HttpVerbsがPOSTであることが基本条件になりました。

セキュリティ的にゴニョゴニョらしいです。

  public ActionResult Json()
  {
    //var json = Json(new {result = "json!"});
    //json.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
    //return json;
return Json(new {result = "json!"},JsonRequestBehavior.AllowGet); }

Getなら↑これで大丈夫。

修正:2010年3月9日

  • DefaultControllerFactoryからRequestContextプロパティが無くなりました。

依存性をなくすのはいいことです。その代わり、Controllerのインスタンスを生成するためのCreateControllerにRequestContextをパラメータとして渡すようになってます。
Mockを作る時が楽ちんです。

  • AreaをサポートするためにRouteData.Valuesで"area"が予約されるようになりました。

自分仕様でこの名前の値を使ってる場合は変更しておきましょう。

  • Html.Substituteは残念ながら使えなくなりました。
  • クライアントサイド検証が標準で組み込まれてます。

このクライアントサイドコードを生成するために、C#からJavaScriptを吐き出す
ScriptSharpが使われています。
コード生成に興味がある方は、参照してみるといいんじゃないかと思います。

Script#
http://projects.nikhilk.net/ScriptSharp

ボスが最近似たようなのを見つけてはしゃいでました。

SharpKit - Write C# instead of JavaScript
http://sharpkit.net/

  • Templateベース

DynamicDataと同じようにTemplateベースのモデルレンダリングを実装したHtml.EditorForやDisplayFor、プロパティベースのレンダリングを行うTextBoxForやLabelForなんかも目が離せない便利機能です。
System.ComponentModel.DisplayNameAttributeやUIHintAttributeを使ってカスタマイズもしやすくていいですね。

注目機能

  • ChildActionOnly属性

HtmlHelperのRenderAction/Actionからの要求しか受け付けないようにするActionFilter。

  • UrlParameter.Optional

Route登録時にRouteData.Valuesにキーすら含ませないようにするオプション。

Deep Dive!

一番注目したいのはASP.NETらしさをしっかり継承したProviderモデルへのリファクタリングです。マニアックな部分ですが、拡張性を考慮して、より自由どの高い開発を行えるよう沢山のフックポイントを提供するために実装されています。処理の流として入力→検証(Model)→処理(Controller & Model)→出力(View)
というフローになるそれぞれのつなぎ部分で拡張できる感じです。

MVCソースから"Provider"と付いているクラスを検索!

  • ModelMetadataProviders
    • ModelMetadataProvider
      • AssociatedMetadataProvider
        • EmptryModelValidatorProvider
        • DataAnnotationsModelValidatorProvider

ModelMetadataを取得するためのプロバイダ。
モデルやモデルプロパティに関するすべてのメタ情報。
ValidationAttributeの定義や、型情報、出力方法など。

  • ModelValidatorProviders
    • ModelValidatornProvider
      • AssociatedValidatorProvider
        標準のValidationAttribute用のAttributeAdopterクラスとDataAnnotationsModelValidatorを管理。
        • EmptryModelValidatorProvider
        • DataAnnotationsModelValidatorProvider
      • DataErrorInfoModelValidatorProvider
        IDataErrorInfoの実装に対するDataErrorInfoPropertyModelValidatorとDataErrorInfoClassModelValidatorを使って、検証の結果を取得する。
      • ClientDataTypeModelValidatorProvider
        数値型に関するNumericModelValidator。
    • DataAnnotationsModelValidatorProvider
    • DataErrorInfoModelValidatorProvider
    • ClientDataTypeModelValidatorProvider
  • IValueProvider
    • NameValueCollectionValueProvider
      • FormValueProvider
      • RouteDataValueProvider
      • QueryStringValueProvider
    • HttpFileCollectionValueProvider

ModelBinderがモデルにデータをバインドするさいに、値を取得する際に利用する。データの出所がどこなのかをModelBinderは意識しなくてもいいんです。

次に"Factory"とついたクラスを検索!

  • DefaultControllerFactory
  • ValueProviderFactories
    • ValueProviderFactory
      • FormValueProviderFactory
      • RouteDataValueProviderFactory
      • QueryStringValueProviderFactory
      • HttpFileCollectionValueProviderFactory

コントローラの生成用のControllerFactoryと、IValueProviderを実装したValueProviderFactoryたちの2種類です。これがリファクタリングした超重要な部分になります。

で、後は、これらを利用したサンプルの紹介という流れです。

MVC Presenter

↑これね。このサンプルそのものがWebアプリケーションになっていて、サンプルの実行そのものがプレゼンスライドの表示になるという仕掛けだったんですけど...。

プロバイダモデルがうんぬんくらいからちょっとツマラナイ話になってしまいましたね。今回はコードに関する説明がごっそりできてないので、MVC2の良くなった部分がサンプルのどこにどう使われているのか、全然伝えられませんでした。反省してます。Zipで圧縮したのをアップロードしてどうのこうのとか、実はパワポじゃないんだよとか、それ以前に何を伝えようとしてるのかが、伝えられてなかったです。ドン引きってこんな時に使うのかな。

心が折れて、帰りのゆりかもめでは夕日が目に染みた。泣いてなんかないやい。

JOJO'S BIZARRE ADVENTURE Part4 Diamond is not Crash

けど、魂を砕くことはできないぜ!また機会があったら、今回の教訓を生かし、もっと参加者に楽しんでもらえるよう的を絞ったプレゼン+コードにしようと思います。

サンプルの説明を近いうちここにエントリしようと思います(ここ知らない参加者の方には申し訳ないですが)。

2010年2月15日月曜日

Sauce IDE+RCはFirefox+Firebugでの使い勝手がとてもいい

Webアプリのテストの定番といえば、SeleniumかWatiNなのかな、というレベルのWebアプリテスト初心者ですが、Seleniumの後継にあたる(この辺あいまい)Sauceっていうのを試してみたよ!

Products - Sauce Labs

まだないけどいつかDimeCastsでビデオが公開されるに違いない。

試すにあたり、何かしらプログラムが必要だろうと、昨日ハンサムスーツを見ながらシンプルブログ(全然ブログじゃないというのは本題じゃないYO!)を作ってみた。

タイトルいれて本文入れるだけね。せっかくだからASP.NET MVC RC2で。話はそれるんだけど、RC2のItem Templateはイマイチかも。TextBoxForなんかは新しいTemplateベースのDisplayForやEditorForに揃えてくれるともうちょっと素敵さが増すんじゃないかな~。せっかくDataAnnotationsでDisplayName属性を指定してもLabelForで展開しないとそこから表示名を取得してくれないじゃないっすか。Listテンプレートは面倒かもしれないけど、そうじゃないところはDataAnnotationsの属性を生かせるヘルパーを使った物に統一してもらいたいデス。せっかくUIHintでカスタムEditorTemplatesを指定しても効いてくれなくてちょっと切なかった。

sauce1

↑一覧ページ。殺風景ですいません。

sauce2 sauce2_2

↑新規ページ。同じフォーマットで編集ページもあるよ。ここで入力エラーがあると、DataAnnotationsがバシっと効いてくれるのがV2の素敵なところ。

sauce3

↑新規登録すると一覧に表示されるよ。殺風景ですいません。

sauce4

↑詳細ページ。コレといって何も無いですね。あ、本文にはHTMLが入るイメージなのでHTMLエディターをEditorTemplatesで作ってもいいね。今ならMarkdownのほうがオシャレかの~。

sauce5

↑削除ページ。削除すると、一覧ページに戻るだけ。

とりあえずこんなのを作って、これに対してテストを実施させてみようじゃないですか。

実行したいテストは以下の通り。

  1. 1件も登録されてない状態で新規登録
  2. 1件以上登録されてる状態で新規登録
  3. 未入力状態でCreateを押してモデル検証をエラーにする
  4. 登録されているすべてのエントリを削除する

今回認証は無しで。

そもそもSeleniumすら使った事ないけど、まずはSauce LabsのProductsトップにあるビデオを確認。ふ~ん、ってなもんです。とにかく、Sauce IDEをFirefoxにインストールして、Sauce RCをローカルPCにインストール。まずはこれでいいっぽい?う~ん残念。Selenium RCをダウンロードしておかないとUnit TestをC#で実行できないみたいです。

Sauce IDEがテストレコーダーで、Sauce RCがテスト実行サーバー、Selenium RCがテストを実行させるのに必要なアセンブリ(Sauce RCサーバーとの通信を行う)を提供、って具合の理解でよろしかろうか。Sauce RCはJavaアプリで、Webベースの管理インターフェースもありそこでテスト実行ブラウザやその他設定の変更ができると。

sauce6

テスト実行する場合、ブラウザを起動してそのブラウザをコントロールすることでテスト実行と結果取得する仕組み?Sauce IDEが表示してくれる、C#のコードはNUnitを基本になってて、そこはMSTestでよしなに動くようにチョチョッと変更。大枠はTestInitializeでDefaultSeleniumを初期化するようにするのと、TestCleanupでDefaultSeleniumの停止処理、あとはひたすらテスト書く。

    private ISelenium selenium;
    private StringBuilder verificationErrors;

    [TestInitialize]
    public void Setup()
    {
      selenium = new DefaultSelenium(
"localhost",
4444,
"*firefox",
"http://localhost:20337/"); selenium.Start(); verificationErrors = new StringBuilder(); } [TestCleanup] public void TeardownTest() { try { selenium.Stop(); } catch (Exception) { // Ignore errors if unable to close the browser } Assert.AreEqual("", verificationErrors.ToString()); }

あと、ちゃんとSelenium RCのダウンロードしたアセンブリをテストプロジェクトでは参照設定しとこうね。

DefaultSeleniumの1個目と2個目のパラメータはなんじゃらホイ。3個目はブラウザっぽくて4個目がテスト対象アプリのアドレス。どうもこの1個目と2個目がSauce RCのサーバーらしい。RCの制御用Webインターフェースはhttp://localhost:8421/ でポートが8421。実際のアプリはポートが20337と、いろいろ仕組みを理解しておかないと~。Sauce OnDemandっていうクラウドサービスもあるからね~。

上記初期コードを書いておけば、あとはIDEの出してくれるC#コードをコピペっちょ!なんだけど、それだと思ったようなエレメントを指してくれない。実行結果のHTML全体に文字列が含まれてるかをチェックするIsTextPresentなんかを使うのはテストとして成立しないっすよね。

なので、エレメントの参照はCSS SelectorかID指定かXPath指定がオーソドックスな指定の仕方になるかと思うわけです。でね、Firebugってさ、前バージョンまでは別途Plug inを入れないとXPathの取得ってできなかったけど新しいバージョンになって”XPathをコピー”っていう機能が標準で実装されてるじゃないですか。

sauce7

↑エレメントのXPathが表示されてるから↓右クリックでコピーだ!

 sauce8

で、対象となるエレメントを特定するためのXPathは何の苦労もなく取得できちゃうので、テストコードのアサーション部分やClick対象のエレメントなんかもこれを使って一意に特定だぜ!

    [TestMethod]
    public void A_First_Entry()
    {
      selenium.Open("/");

      selenium.Click("link=Create New");
      selenium.WaitForPageToLoad("30000");
      selenium.Type("Title", "first");
      selenium.Type("Body", "最初のエントリ");
      selenium.Click("//input[@value='Create']");
      selenium.WaitForPageToLoad("30000");

      Assert.IsTrue(
        selenium.GetText("xpath=/html/body/div/div[2]/table/tbody/tr[2]/td[2]") 
        == 
        "first");

      // 最初のDetailsをクリック
      selenium.Click("xpath=/html/body/div/div[2]/table/tbody/tr[2]/td/a[2]");
      selenium.WaitForPageToLoad("30000");

      Assert.IsTrue(
        selenium.GetText("xpath=/html/body/div/div[2]/fieldset/div[4]")
        ==
        "最初のエントリ");
    }

↑こんな感じで入力値と入力結果を比較してちゃんとHTML的に想定通りの結果が生成されてることをテストコードに書けちゃうのが素晴らしい。無知って怖い。これまで自分は一体なにをしてたんだと言いたい。

これらテストを実行する時に、MSTest(かReSharperのテストのどっちがそういう仕様なのかは知らないけど)だとテストメソッド名順に実行されるので、テスト内容が直前の実行結果に依存するような場合は、プレフィックスを決めておく必要があったりしそう。テストクラスそのものを分けてしまうのもありだけどね。

実行すると、ブラウザが起動してあとは勝手に処理してくれるっす。

sauce9  sauce10

↑これがFirefoxで実行してるところ。

sauce11

↑こっちがIEで実行してるところ。

ブラウザの切り替えはTestInitializeの設定じゃなくて、Sauce RCの設定画面が優先、っぽい。何にせよ、初めてのWebアプリのユニットテストとしては好感触。今後も精進していきます。

今回のサンプルアプリとテストコードは↓こちらからどうぞ。

※ちなみにサンプルアプリ自体のテストコードはないのであしからず。

dotnetConf2015 Japan

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