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でなんかおかしくなったんじゃないかな。気をつけてね。

dotnetConf2015 Japan

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