blog.ishinao.net

142月/12

iOSアプリでUDIDを使わないようにする簡単な方法

また最近iOSのUDIDがらみの話をよく見かけるんで、もっとも手間をかけずにUDIDからアプリケーションUUIDに移行する方法を

https://gist.github.com/1824855

に書いてみた。

gistの使い方がよくわからないんだが、これライセンスとかどうなってるんだ? 内容は、https://gist.github.com/1161447https://gist.github.com/1743326を足しただけなんで、俺独自のコードはない。github上でforkしたかったんだけど2つのコードの内容を足すときに、どう表現すればいいのかよくわからなかったんで、新規gistにした。

使い方としては、すでにUDIDを使っているiOSアプリのXcodeプロジェクトにこの2ファイルを追加するだけ。

NSString *udid = [[UIDevice currentDevice] uniqueIdentifier];

みたいにUDIDを取得しているつもりのコードが、自動的にアプリケーション+デバイスユニークなUUIDに差し替わる。自動生成されたUUIDはKeychainに保存されるんで、アプリケーションの再インストールをしても維持される。

UUIDをリセットしたい場合は、

[[UIDevice currentDevice] resetUniqueIdentifier];

すればいい。

これくらい簡単に差し替えられるならば、すでにUDIDを使っちゃってたアプリも気軽にUDID不使用バージョンにできるよね。

Filed under: 技術日記 No Comments
1912月/11

さくらVPS+CentOS+PHPでCatchAllなメール受信

試しにやってみたらやたらとはまったのでメモ。

さくらのVPSに適当なバーチャルホスト(foo.example.com)を割り振り、そのバーチャルホスト宛に届いたすべてのメール(*@foo.example.com)をPHPスクリプトで受信したい。

CatchAllじゃないんだったら、受信したいアカウントの.forwardとかからスクリプトを呼び出せばいいんだけど、CatchAllとなるとまず受信したいアカウントというものが存在しないわけで、その辺から設定する必要がある。

具体的には、まず適当な受信用のアカウント(mailreciever)を用意する。


useradd -s /sbin/nologin mailreciever

で、そのアカウントとバーチャルホストのCatchAll転送先を結びつける。まずはバーチャルホストをsendmailが受信するドメインとして追加するために、/etc/mail/local-host-namesに以下を追加。


foo.example.com

そして、そのドメインのCatchAll転送先としてmailrecieverアカウントを登録するために、/etc/mail/virtualusertableに以下を追加。


@foo.example.com mailreciever

virtualusertableはコンパイルが必要なので、


yum install sendmail-cf # 必要ならば
makemap hash /etc/mail/virtualusertable.db < /etc/mail/virtualusertable

mailrecieverアカウントに受信したときにPHPスクリプト(/path/to/script.php)を実行するように/etc/aliasesに以下を追加。


mailreciever: "|/usr/bin/php /path/to/script.php"

というのは実はうまく動かなかった。smrsh環境の制約に引っかかるらしい。そこで、


ln -s /usr/bin/php /etc/smrsh/php

のように/etc/smrshディレクトリ内にPHPのCLIバイナリのシンボリックリンクを作っておいて、先ほど書いた/etc/aliasesの内容を、


mailreciever: "|/etc/smrsh/php /path/to/script.php"

と変更するとsmrsh環境が原因のエラーを回避できる。

ちなみに/etc/aliasesもコンパイルが必要なので、


newaliases

を実行してコンパイルしておく。

これでlocalhost上で*@foo.example.com宛てのメールを送ると、ちゃんと/path/to/script.phpが起動できるようになったのだが、外部からメールを送ってもちゃんと着信していない。

ああ、もちろんそれ以前に、foo.example.comのドメインを該当サーバーのIPアドレスでDNS登録しておく必要があるけど、そういう問題ではない。

あと、自分でセットしたソフトウェアファイアウォールで25ポートINがふさがっているのかと思ったりもしたが、それもない。さくらインターネットが25ポートINを外側でふさいでいるのかと思ったのだが、それでもない。

正解は、さくらVPS+CentOSでインストールされるsendmailのデフォルト設定は、localhostからのSMTPしか受信しないようになっている、だった。

sendmail.cfには触らないと心に誓っているんで、あきらめてpostfixにでも入れ直そうかと思ったんだけど、ググってみたらそれほど設定変更は大変そうじゃなかったんで、sendmailのまま設定を変えて対応した。

/etc/mail/sendmail.mcの


DAEMON_OPTIONS('Port=smtp,Addr=127.0.0.1, Name=MTA')dnl

という行をコメントアウトして、


dnl # DAEMON_OPTIONS('Port=smtp,Addr=127.0.0.1, Name=MTA')dnl

に変更し、


m4 /etc/mail/sendmail.mc /etc/mail/sendmail.cf

でコンパイルしてからsendmailを再起動したら、無事外部からのSMTP接続も受信してくれるようになり、foo.example.com宛てのすべてのエールを/path/to/script.phpで処理できるようになった。

なんかものすごくはまりどころが多かった。

Filed under: 技術日記 No Comments
912月/11

SilverlightでのWeb APIアクセス処理(無駄な試行錯誤の過程)

なんかもう、Silverlightの普通の.NET Frameworkとの互換性のなさにはうんざりしているんだけど、その中でも特にうんざりしたWebRequest周りについて。

普通に.NET FrameworkでWeb APIとかにアクセスしたい場合、System.Net.WebClientとかを使うと簡単にできる。


var client = new System.Net.WebClient();
var url = "http://example.com/path/to/api";
var values = new NameValueCollection(){{"key", "value"}};
var byteResult = client.UploadValues(url, values);
var stringResult = Encoding.UTF8.GetString(byteString);

受け取った文字列がJSONだった場合は、


var serializer = new JavaScriptSerializer();
var result = serializer.Deserialize(stringResult, typeof(ObjectForMapping);

みたいな感じで受け取り用のクラス(ObjectForMapping)を用意しておいて、それにデシリアライズしたりとか。 あと、これだけだとCookieベースの認証とかが維持できないんで、webclient 継承 cookiecontainerでググると出てくるような感じで、


class MyClient: WebClient
{
  protected CookieContainer _cookie = new CookieContainer();
  protected override GetWebRequest(Uri address)
  {
    var request = base.GetWebRequest(address);
    if (request is HttpWebRequest) {
      (request as HttpWebRequest).CookieContainer = _cookieContainer;
    }
    return request;
  }
} 

みたいな感じでCookieを扱えるクラスを作っておくと、自動的にCookieを維持してくれるようになる。 でまあ、Silverlightでも同じようなことができるよね、と思って、クラスライブラリの名前空間とかファイルとかの構成がいろいろ変わっているのを乗り越えて、


using System.Windows.Browser;
using System.Net.Browser;
class MyClient: WebClient  
{
} 

とやったところ、これはコンパイル自体は通るくせに、new MyClient()するコードブロックを通ったとたんに、何のエラーメッセージも出さずにそっと落ちる。 どうやらWebClientを継承したらだめなようだ。

けど、そんなドキュメント、どこにも見つからないし、だいたいコンパイルは通ってるんだけどなー。っつーか、結局WebClientを継承するだけでもだめだ、と気づくまでどれだけ時間がかかったことか。 しょうがないんで、自前でWebRequestを使って処理を行うような構成に変更することにする。必要に応じて、


var request = WebRequest.Create(new Uri(url));
(request as HttpWebRequest).CookieContainer = _cookie; // 使い回すCookieContainer
request.BegubGetResponse(callback, request);

みたいな感じね。でも、これが通らない。なぜかというと、


var request = WebRequest.Create(new Uri(url));

で返されるWebRequestがHttpWebRequestではなくBrowserHttpWebRequestってやつになっている。なんじゃそりゃ。それはどの辺のドキュメントに書いてあるんだよ。MSDN内で検索してもそのドキュメントが見つからないぞ。

いろいろ探し回った結果、方法: Cookie の取得と設定を行うというドキュメントを見つけた。解説自体は今までやっていたことを説明しているだけで、特に新しい情報はない。しかし、サンプルコードに見慣れない処理が入っている具体的には、


IWebRequestCreate creator = WebRequestCreator.ClientHttp;
WebRequest.RegisterPrefix("http://", creator);
WebRequest.RegisterPrefix("https://", creator);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://api.search.live.net/clientaccesspolicy.xml")

なんじゃこりゃ。WebRequestCreatorクラスのドキュメントを見てみると、

Silverlight では、ブラウザーとクライアントのどちらで Silverlight ベース アプリケーションの HTTP 処理を行うかを指定できます。既定では HTTP 処理はブラウザーによって行われるため、クライアントで HTTP 処理を行う場合は選択の必要があります。通常、HTTP の要求と応答の処理方法を指定するには、このクラスのプロパティを WebRequest の RegisterPrefix メソッドSilverlight では、ブラウザーとクライアントのどちらで Silverlight ベース アプリケーションの HTTP 処理を行うかを指定できます。既定では HTTP 処理はブラウザーによって行われるため、クライアントで HTTP 処理を行う場合は選択の必要があります。通常、HTTP の要求と応答の処理方法を指定するには、このクラスのプロパティを WebRequest の RegisterPrefix メソッドに渡します。に渡します。

なるほど、Silverlightってそこまでブラウザと密結合していたのね。 標準ではBrowserHttpWebRequestってのが使われるから、非Silverlight環境と互換性のあるHttpWebRequestを使いたい場合は、サンプル通りにSilverlight側でhttpスキームを処理するように指定してからWebRequest.Createしなければならないのか。そうやったらちゃんとHttpWebRequestが生成されるようになった。CookieContainerも使えている。

で、一通りコンパイルは通るようになったし、実行もできるようになったんだけど、なぜかCookieによる認証が維持されない。認証CookieがHttpOnlyなんで、中身をのぞけないからよくわからないが、少なくとも同じようなコードで通常の.NET Frameworkでは動作するものが、Silverlight環境ではうまくいかない。

うがー、どないなっとんねん。と、SilverlightのHTTP通信周りのドキュメントをもう一度良い返してみる。そうしたら、Silverlightの標準の方のBrowserHttpWebRequestでは、ブラウザの機能を使ってHTTPリクエストを処理するんで、ブラウザ側のCookieがそのまま使えるっぽい? なんか非Silverlight用に作ったコードを移植しようとしていたのは無駄な努力だった?

というわけで、単純に認証Cookieを保持したいだけだったら、わざわざHttpWebRequestを生成して自前でCookieContainerを処理したりする必要はなく、何も考えずにSilverlight標準のWebアクセスクラスを使ってしまえば、ブラウザ相当のCookie処理が使われてくれる。

先に非Silverlight用のWeb APIクライアントコードを書いて、それをSilverlightでも動くように移植しようとしたせいで、無駄にはまったんだね。はじめからSilverlight用に書いていれば何も考えなくても動くものが作れた気がする。ただし、ブラウザ互換のWebアクセスクラスにも制約があるんで、それで全部いけるわけではないけど。

書き忘れていたけど、クロスドメインアクセス用のcrossdomain.xmlとかは、Flashとかと同様、サーバーサイドにあらかじめ用意しておく必要がある。

で、APIアクセスライブラリとしては一通り動くようになったから、SilverlightアプリケーションからWeb APIをコールして、その結果をTextBoxに表示するというテストアプリを作ってみた。が、「System.UnauthorizedAccessException: 無効なスレッド間アクセスです。」というエラーが出て表示できない。

どうやらAsyncCallbackから呼び出されるスレッドと、UI部品などを管理するメインスレッドとは別スレッドだから、直接は呼び出せないと言うことらしい。

いろいろググったところ、[Silverlight 奮戦記] (3,4) ファイルを読む β2 対応版という情報を見つけた。AsyncCallbackから呼び出されたメソッドから、さらにSyncronizationContect.Current.Post()を使ってメインスレッドとの同期を取ってから、メインスレッド側のパーツに対する処理を行えば回避できるみたいだ。でも、この辺の情報は公式ドキュメントではどの辺に書いてあるのかなー。

といった感じで、.NET Frameworkがらみのドキュメントは非常にわかりにくいし、同じ名前のクラスが多いくせに制限事項が違うSilverlight環境の場合は、それがさらにひどくなっているんで、あまり深入りしたくない気持ち満点になった。デバッグ環境としてできがいいVisualStudioなのに、Silverlightのデバッグだとうまく動かないことがあまりにも多いし。

Filed under: 技術日記 No Comments
712月/11

Eclipse使っている人はSubversion 1.7系へのアップデートはもうちょい待ったほうがいいかも

追記@2012/02/03)解決したよ→今日の検索キーワード:ASP.NET MVC3、Subversion 1.7+Eclipse動いた

212月/11

Zend_Frameworkにおけるバリデーションコードの置き場所

Zend_Frameworkにおけるバリデーションコードの置き場所ってどうよ? ってのをいろいろ試行錯誤した結果、今のところこうしてるよ、というのをまとめてみる。

DBアクセスレイヤーは基本的にZend_Db_Tableを使っている。テーブルにマッピングできないものも、データ操作レイヤーに関するものはapplication/models/以下に置いて、Zend_Db_Tableと同じようなインターフェースを持たせて使う。で、共通化できるバリデータはこのモデルクラスにまとめて持たせる。


class ModelBase extends Zend_Db_Table
{
  protected $_validators = array();
  public function getValidators()
  {
    return $this->_validators;
  }
  public function getValiator($name)
  {
    if (!isset($this->_validators[$name]) {
      $this->_validator[$name] = new Zend_Validate();
    }
    return $this->_validator[$name];
  }
}

みたいなベースクラスを作っておいて、 各実装クラスでは、


class FooTable extends ModelBase
{
  public funciton init()
  {
    $this->getValidtor('foo')
    ->addValidtor(new Zend_Validate_StringLength(5, 15));
  }
}

みたいな感じでinit()内でバリデータをセットしておく。そうすると、Zend_Db_Table_Rowとかでこのバリデータを使いたい場合は、


class FooTable_Row extends Zend_Db_Table_Row
{
  public function isValid()
  {
    $result = true;
    foreach ($this->getTable()->getValidators() as $name => $validator) {
      $result = $validator->isValid($this->$name) && $result;
    }
    return $result;
  }
}

なんて感じでバリデーションを書ける(上の例は共通バリデータをぶん回すだけでOKな場合の話ね)。また、このFooTableに対してのCRUDを行うようなZend_Formでは、


class FormFoo extends Zend_Form
{
  public function init()
  {
    $fooTable= new FooTable();
    $foo = $this->createElement('input', 'foo');
    $foo
    ->addValidator($fooTable->getValidator('foo'))
    ->addValidator(new CustomValidatorForForm());
    $this->addElement($foo);
  } 
}

なんて感じでFooTableの該当パラメータ用のバリデータを使うことができるし、それにフォーム専用の追加パラメータを追加することもできる。

アクションコントローラのパラメータの場合は、


class FooController extends Zend_Controller_Action
{
  public function fooAction()
  {
    $foo = $this->_getParam('foo');
    $fooTable = new FooTable();
    $validator = $fooTable->getValidator('foo');
    if (!$validator->isValid($foo)) {
      // IIIINNNNVALIIIIIIIDDDDDDDDDDD!!!!!!!!!!!!!!!!
    }
  }
}

なんて感じで、使いたい要素のバリデータだけ引っ張り出して使える。

ということで、バリデータはモデルクラスにパラメータ名ごとに共通(使い回せる)部分を突っ込んでおいて、シチュエーションごとにそれを取り出しつつ、シチュエーションに特化したバリデータはその場でaddValidator()して使う、いう置き方が一番きれいなんじゃないかと思うのだが、どうだろう。

フィルターに関しては、まとめて同じ処理をしたいという要求がそれほど大きくない、というか同じパラメータでも入力シチュエーションによってどういうフィルターをかけたいかが変わるんで、必要な場所(アクションメソッドとかフォーム要素とか)にそれぞれ書くのが無難な気がする。フィルタークラス自体はアプリケーションに特化したものをapplication/filters以下に用意しておいた方が楽だけど。

と、久しぶりにZend_Frameworkで新しいアプリを書こうと思い、どうせならZend_Frameworkのコンポーネントを使い倒した書き方にしてみるかといろいろ試行錯誤した結果、こんな感じになったという話でした。

Windows Server 2008でManaged DirectXを使ったASP.NET MVC3が動かないという、ググってもレアケース過ぎて解決策が見つからないシチュエーションからの現実逃避の一環でもある。単にDirect 3Dのベクトル演算関数をサーバーサイドでも使いたいだけなんだけどなー。なんで依存関係が解決できないんだろう。

112月/11

多重のセキュリティとか出力時のバリデーションとか

なぜPHPアプリにセキュリティホールが多いのか?:第44回 セキュリティ対策が確実に実施されない2つの理由|gihyo.jp … 技術評論社

gihyo.jp セキュリティ対策が確実に実施されない2つの理由

はてなブックマーク - 第44回 セキュリティ対策が確実に実施されない2つの理由|gihyo.jp … 技術評論社

なんではてブでこんなに叩かれているんだ? Rails関連はよう知らんけど、それ以外の内容で特に叩かれるようなことは書いてないと思うんだけど?

「多重のセキュリティ対策」って言葉に引っかかっている人がいるみたいだけど、単に「既にチェック済み,これは数値だからエスケープ処理は不要,などとの誤った判断からエスケープ処理を行っていないコード」みたいな事例に対して、入力値でバリデーションしたから出力値はエスケープしなくていいとか考えるなって言っているだけでしょ。

で、上記みたいな誤った判断をする人がその理由として、「だってDRYって言うし、いったんこの入力値に対してはチェックコードを書いたから、出力時の処理は書かなくていいよね」とか言いかねないから、「セキュリティ対策とコーディングのベストプラクティスは相反する」と言ってるんじゃないの? 結局ここで言うベストプラクティスってのは「重複処理を排除した効率よいコード記述」のことみたいだし。「それを重複と考えるのはおかしい」とか言っても、それを重複と考えるような馬鹿を前提とした文章だって読み取れるじゃん。

あと出力時のバリデーションに関しては、「1と2の対策が不可能な場合は出力時にバリデーション処理を行う」と書いてあるとおり、どうしてもエスケープなどを使った安全な出力ロジックが使えない場合は、しょうがないから出力に関してもバリデーションしようね、という話になっていて、別に推奨しているわけじゃないし、実際特殊な環境向けの出力ではあり得る話だと思うんだけど。

それにしても「セキュリティ対策」って言葉は曖昧で良くないな。

  1. 普通に動作させるために必要な設計と実装(そして、それができなかった際に発生するバグ)。
  2. 情報漏洩や情報破壊を引き起こしうるバグに対する注意。
  3. 積極的なセキュリティアタックに対しての対抗策。

それらが全部なんとなく「セキュリティ対策」でくくられてしまうけど、全部混ぜて議論すると焦点がぼやけてぐだぐだになる。どこまでわかっている(問題にしている)人に対して、何を伝えたいのかがそれぞれ違ってくる。

Filed under: 技術日記 No Comments
3011月/11

Zend_View_Filterで文字コード変換するときの問題

前提条件

Zend_Frameworkで、内部エンコーディングとしてUTF-8を利用しているWebアプリケーションを構築していて、一部ページのみガラケー(SHIFT_JIS)対応したい。

問題

Zend_View_FilterでUTF-8からSHIFT_JIS変換するフィルターを作り、ガラケー用コントローラのinitで


public function init()
{
  $this->view->addFilter('文字コード変換フィルター名');
}

とかしてみた。すると、一見ちゃんと文字コード変換がかかったかと思いきや、一部文字化けしている。

調べてみると、addFilterしたフィルターはZend_Viewがレンダリング処理を走らせるたびに実行されるんで、レイアウトとかを使って内部で複数回Zend_View::renderされてしまうと、その部分で複数回文字コード変換がかかる→文字化けする、ということらしい。

ZendFrameworkで文字コード変換 - slumbersでは、Zend_Layout_Controller_Plugin_layoutを拡張したクラスで出力時に変換をかけたらうまくいったよって話が書いてあるけど、もうちょい楽な方法ないかなー。

解決策

複数回フィルターがかかるのがいやなら、出力する文字列が最後まで確定した後にフィルターがかかればいいんじゃないか、ということで、ガラケー用のレイアウトファイルの一番最後に、


<html>
....(省略)
<?php echo $this->layout()->content; ?>
....(省略)
</html>
<?php $this->addFilter('文字コード変換フィルター名');

とかしてみた。うまくいった。

2411月/11

Entity SQLで「クエリの結果を複数回列挙することはできません。」エラー

Entity SQLで取ってきたデータに対して、まずCount()で全件数を取得してから、ページング用の実データをSkip、Takeするような処理を書き、それをforeachで回そうとすると、「クエリの結果を複数回列挙することはできません。」エラーが出る。

擬似コードで言うところの、


var db = new dbEntity();
var sql = "select * from table order by id";
var parameters = new List<MySqlParameter>();
var result = db.ExecuteStoredQuery(sql, parameters);
int count = result.Count();
int page = 1;
int pagesize = 20;
int pagecount = (int)(count / pagesize) + 1;
foreach (var row in result.Skip( pagesize * (page - 1)).Take(pagesize))
{
  // ...
}

なんて感じの処理。

必ず出るわけでなく、出たり出なかったりするのだが、どうやらその条件は「パラメタライズされたクエリーかどうか」らしい。上記みたいにparametersが空の場合は問題なく動く。動的にwhere条件を組み立てたりして、parametersにその条件値を入れていたりすると、foreachのところで「クエリの結果を複数回列挙することはできません。」が出る。

LINQ to SQLの仕組み(ほぼEntity SQLと同じ仕様だと思われる)がよくわかっていないんで、内部的にどのタイミングでどういうSQL文が発行されているのかがわからないんだけど、LINQ to SQLのドキュメントを斜め読みした限りでは、実際のSQL文の発行はできるだけ遅延実行するようになっているらしいから、Count()とかTake()とかforeachでEnumerableにしたタイミングで、実際にSQL文を発行しているのかな?

で、Count()とかで一度SQL文を実行して、パラメータ埋め込みを実行してしまったあとに、もう一度SQL文を発行しなければならない処理を呼び出すと、「クエリの結果を複数回列挙することはできません。」エラーが出ている? どうもすっきりしないけど、たぶんそんな感じのことが起こっているのではないかと推測。

ともかく、内部で何が起こっているのかよくわからないEntity SQLの機能を使うのはやめて、SQL文を組み立てるときに件数取得SQLと実体取得SQLの二つを生成しておいて、それぞれ呼ぶようにして回避。擬似コードで言うと、


var countSql = "select count(*) as cnt from table";
var selectSql = "select * from table";
var parameters = new List<MySqlParameter>();
var count = db.ExecuteStoredQuery(countSql, parameters).First<int>();
var result = db.ExecuteStoredQuery(selectSql, parameters).Skip(offset).Take(limit);

みたいな感じね。countみたいなデータを、モデルクラス以外の型でどうやって受け取ればいいのかには、しばらく悩んだけど、こんな感じでintで受けるといけるようだ。

Filed under: 技術日記 No Comments
2411月/11

MySQL Connector Netの非互換性

昨日書いた「Entity SQLでtritonnの全文検索を行う « blog.ishinao.net」の話の続き。ローカル開発環境でだいたい動くようになったんで、サーバー環境でもちゃんと動くか試してみようと、サーバーに作りかけのアプリをセットアップしてみた。

ローカル開発環境のMySQL Connector Netのバージョンは6.4.3だったんだけど、MySQLのWebサイトに載っている最新版は6.4.4になっていて、まあマイナーバージョンの最後の一桁が1違うくらいならば、大して問題は出ないだろうとサーバーサイドは6.4.4をインストール。

で、動かしてみたらローカル開発環境では問題なかったのに、「MySqlParameter以外は受け取らないよ」エラーが、ExecuteStoreQueryの第2引数で出る。MySqlParameterをちゃんと渡しているんだけどなーと不思議に思い、明示的にキャストしてみたり、MySqlParameter[]な変数を定義してそれにコピーしてから渡してみたり、いろいろ試してみたけれども動かない。

おかしいなーと、試しにローカル開発環境のMySQL Connector Netも6.4.4にアップデートしてみたら、同じエラーが出るようになった。なんじゃそりゃ。6.4.3と6.4.4の間でそんな非互換性があるの? あるいはバグ?

よくわからんが、そこを追求する気にはなれなかったんで、サーバーも開発環境も6.4.3に合わせたら、どちらでもちゃんと動くようになった。MySQL Connector Netを使うときにはバージョンに要注意。

Filed under: 技術日記 No Comments
1811月/11

ASP.NET MVC 3でCRUDするときのHtmlHelper用テンプレート

CRUD機能を自動生成するとビュー内で使われている、HtmlHelperのEditorForメソッド。モデルオブジェクトから各要素の型や名前を取得して、妥当な編集用フォームタグを生成してくれるのはいいんだけど、気軽にカスタマイズさせてくれないようだ。

たとえば、テキストの分量によってinput type="text"にHTML属性を追加してsize="80"とかしたり、長文テキストが入るからtextareaに差し替えようと思っても、メソッドにパラメータを追加して何とかするような方法は用意されていない。

表示をカスタマイズしたければ別途テンプレートファイルを用意して、それを使ってレンダリングするようにしてやらなければならない。

HtmlHelper.EditorForメソッドの中の、


public static MvcHtmlString EditorFor<TModel, TValue>(
  this HtmlHelper<TModel> html,
  Expression<Func<TModel, TValue>> expression,
  string templateName
)

みたいにtemplateNameパラメータをシグニチャに持つタイプ。このテンプレート関連の仕様が今ひとつわからないので調査。

EditorForModel メソッド (HtmlHelper, String)によれば、まずテンプレートファイルの検索順序としては、

templateName パラメーターと名前が一致するテンプレートが、コントローラーの EditorTemplates フォルダーにある場合は、そのテンプレートが、モデルのレンダリングに使用されます。 コントローラーの EditorTemplates フォルダーにテンプレートが見つからない場合は、templateName パラメーターの名前と一致するテンプレートが Views\Shared\EditorTemplates フォルダーで検索されます。 テンプレートが見つからない場合は、既定のテンプレートが使用されます。

となっている。アプリケーション全体で共用したい場合は、Views\Shared\EditorTemplatesフォルダ内、各コントローラごとに同名のテンプレートを使い分けたい場合は各コントローラ用のViewフォルダ内(FooController.csで使うならばViews\Foo\EditorTemplatesフォルダ)に置くと。

さらに、モデルクラスのプロパティのデフォルトテンプレートを指定したければ、


[UIHint("FooTemplate")]
public string FooProperty {get; set;}

なんて感じでUIHint属性をつけると、自動的にそのテンプレートが使われると書いてある。けど、元のモデルクラスがADO.NET Entity Frameworkで自動生成されたクラスの場合は、どうすればいいんだ?

普通に考えたら、自動生成されるpartial classで定義されるプロパティに、別ファイルでpartial classを定義して同名プロパティに属性を追加する方法、なんてなさそうだけど。実際やってみたら曖昧な二重定義扱いにされてコンパイルが通らないし。

と思ったら、チュートリアル: ASP.NET MVC でのテンプレート化されたヘルパーを使用したデータの表示というドキュメントを発見。その中の、


using System.ComponentModel.DataAnnotations;
namespace MvcTmpHlprs {
  [MetadataType(typeof(ProductMD))]
  public partial class Product {
    public class ProductMD {
      public object SellStartDate { get; set; }
      [UIHint("rbDate")]
      public object SellEndDate { get; set; }
      [DataType(DataType.Date)]
      public object DiscontinuedDate { get; set; }
      [ScaffoldColumn(false)]
      public object ModifiedDate { get; set; }
      [ScaffoldColumn(false)]
      public object rowguid { get; set; }
      [ScaffoldColumn(false)]
      public object ThumbnailPhotoFileName { get; set; }
    }
  }
}

というサンプルコードが肝。

ADO.NET Entiry Frameworkで自動生成されたpatial class(サンプルではProduct)にMetadataType(typeof(メタデータを定義するクラス名))という属性をつけておき、メタデータを定義するクラス(サンプルコードではProductMD)では、追加属性をつけたいプロパティのシグニチャと追加する属性(UIHintとか)を定義する。すると、ほかのファイルで実装されているpartial classのプロパティに対して、別ファイルから属性を追加することができるようだ。うわー、気色悪い仕様。

でもまあこれで、ADO.NET Entity Frameworkで自動生成されたモデルクラス(テーブル)のプロパティ(カラム)に対して、その表示や編集に使用されるビューテンプレートを指定できるようになった。実装コードとしてはとても気色悪いし、可読性も悪すぎるけれども。

ちなみにテンプレートファイルの記述方法(特にモデルデータの受け渡し方法)も、今ひとつわからないのだが、基本的にはビューを追加する際に出てくるダイアログで、「厳密に型指定されたビューを作成する」&「部分ビューとして作成する」を選択し、「モデルクラス」として対応するプロパティの型を指定してやればいいんだろう。そうするとビュー内ではModelという名前でプロパティの値にアクセスできる。

Filed under: 技術日記 No Comments