Tags: 技術日記

技術日記
MySQL Connector Netの非互換性Edit

昨日書いた「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を使うときにはバージョンに要注意。

Published At2011-11-24 20:19Updated At2011-11-24 20:19

技術日記
Entity SQLでtritonnの全文検索を行うEdit

Entity FrameworkでMySQL+tritonnを使って全文検索を使おうとして、無駄に長時間はまってしまったので、その件について。

Entity Frameworkでmatch against相当のSQL文を発行するには、Entity SQLを使ってネイティブSQL文を発行する必要がある。けど、なんかその辺のドキュメントがいまいちまとまっていない。

実際に使用するコマンドは、ExecuteStoreQuery(TElement) メソッド (String, Object[])で、シグニチャは、

public ObjectResult<TElement> ExecuteStoreQuery<TElement>(
    string commandText,
    params Object[] parameters
)

で、第2パラメータがObject[]になっていて、

parameters の値には、DbParameter オブジェクトの配列またはパラメーター値の配列を使用できます。 値だけが提供されている場合は、配列内の値の順序に基づいて DbParameter オブジェクトの配列が作成されます。
なんて書いてある。でも、DbParameterオブジェクトのコンストラクタは、

public abstract class DbParameter : MarshalByRefObject,
    IDbDataParameter, IDataParameter

と、抽象クラスになっているんで、直接生成できない。じゃあいったい何を使えばいいんだよ。

いろいろ探していたら、SqlParameterというのを見つけた。そこで、

var db = new dbContext(); 
string sql = "select * from table where match (field) against(@keyword)";
var parameters = new List<SqlParameter>();
parameters.Add(new SqlParameter("keyword", keyword));
var result = db.ExecuteStoreQuery<Table>(sql, parameters);

なんて感じで書いてみたが、「Parameter '@keyword' must be defined.」となってうまくいかない。 ちゃんとkeywordって名前のパラメータを追加しているのに何でだろう、ということでものすごく悩んだのだが、結局しょうもないミスだった。上の最後の行を、

var result = db.ExecuteStoreQuery<Table>(sql, parameters.ToArray());

とすれば動いた。そういやList<SqlParameter>で宣言していたんだから、Object[]に明示的に変換しなければならないのか。

ExecuteStoredQueryの第2引数が、paramsキーワードがついた可変長引数で宣言されているから、配列以外が渡されたときに自動的に可変長引数扱いになってしまったせいで、パラメータの型違いエラーもでず、単にkeywordが定義されていないとか言われてしまう。おかげで使っているオブジェクトの種類がだめなのかとか、長々悩んでしまった。実際に発行されるSQL文がどうなっているのかも簡単に知る方法が用意されていないから、どのレイヤーでのエラーなのかもわかりにくいし。

で、動かしてみたら今度は、MySqlParameterじゃなきゃだめだというエラーが出た。そうかSqlParameterってMS SQL Server向けのクラスなのか。MSプロダクトだとSQLほにゃららはMS SQLのことを指しているんだけど、字面的には汎用のRDB向けクラスのように見えるからわかりにくい。そこで、SqlParameterの部分をMySql.Data.MySqlClient.MySqlParameterに変える。最終的には、

var db = new dbContext(); 
string sql = "select * from table where match (field) against(@keyword)";
var parameters = new List&lt;MySqlParameter&gt;();
parameters.Add(new MySqlParameter("keyword", keyword));
var result = db.ExecuteStoreQuery<Table>(sql, parameters.ToArray());

とやることで動いた。なんかものすごく解決に時間がかかった。

ちなみに上記は@parameterName形式でSQL文中にパラメータを埋め込みたい場合の話で、{0}、{1}みたいにパラメータインデックス(パラメータ配列の序数)でパラメータを埋め込む場合は、

var db = new dbContext(); 
string sql = "select * from table where match (field) against({0})";
var parameters = new List&lt;object&gt;();
parameters.Add(keyword);
var result = db.ExecuteStoreQuery<Table>(sql, parameters.ToArray());

でいける。なんかもうたったこれだけのことでものすごくはまった。

ちなみにこうやってEntity SQLを使って検索した結果でも、ちゃんとEntity Objectに変換して扱えるんで、各行オブジェクトの機能に関してはADO.NET Entity Frameworkで自動生成されたモデルクラスを拡張する方法 « blog.ishinao.netで拡張したものをそのまま使える。

Published At2011-11-22 21:12Updated At2019-12-30 15:28

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

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

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

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

HtmlHelper.EditorForメソッドの中の、

[sourcecode language="c#"]
public static MvcHtmlString EditorFor(
  this HtmlHelper html,
  Expression> expression,
  string templateName
)
[/sourcecode]
みたいにtemplateNameパラメータをシグニチャに持つタイプ。このテンプレート関連の仕様が今ひとつわからないので調査。

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

templateName パラメーターと名前が一致するテンプレートが、コントローラーの EditorTemplates フォルダーにある場合は、そのテンプレートが、モデルのレンダリングに使用されます。 コントローラーの EditorTemplates フォルダーにテンプレートが見つからない場合は、templateName パラメーターの名前と一致するテンプレートが Views\Shared\EditorTemplates フォルダーで検索されます。 テンプレートが見つからない場合は、既定のテンプレートが使用されます。
となっている。アプリケーション全体で共用したい場合は、Views\Shared\EditorTemplatesフォルダ内、各コントローラごとに同名のテンプレートを使い分けたい場合は各コントローラ用のViewフォルダ内(FooController.csで使うならばViews\Foo\EditorTemplatesフォルダ)に置くと。

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

[sourcecode language="c#"] [UIHint("FooTemplate")] public string FooProperty {get; set;} [/sourcecode]

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

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

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

[sourcecode language="c#"]
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; }
    }
  }
}
[/sourcecode]
というサンプルコードが肝。

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

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

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

Published At2011-11-18 20:09Updated At2011-11-18 20:09

技術日記
ASP.NET MVC 3で複数フォーマット対応APIを書くEdit

APIコールしたときの結果として、パラメータとして?format=xmlと書いたらXML形式で、?format=jsonと書いたらJSON形式で結果を返すようなやつを、どう書けばいいのか。

もちろんそれぞれ別々にアクションメソッドを用意するとか、ビューだけを差し替えるとかいろいろ手があるけれども、JSONに関してはJsonResultという便利なクラスが用意されているんで、それを使いたい。 元となる結果データは、シリアライズされること前提でクラスで定義することにした。

[sourcecode language="c#"]
public class ApiResult
{
  public string StringResult;
  public int IntResult;
  public bool BoolResult;
  public StringDictionary parameters = new StringDictionary();
}
[/sourcecode]
こんな感じの結果クラスを用意しておいて、
[sourcecode language="c#"]
public  ActionResult SomeApiCall(string format)
{
  ApiResult result = new ApiResult();
  // resultにいろいろセットする
  switch (format) {
    case "json":
      return Json[result];
    case "xml":
      var serializer = new XmlSerializer(typeof(ApiResult));
      var stream = new TextWriter(new StringBuilder());
      serializer.Serialize(stream, result);
      var xmlResult = new ContentResult();
      xmlResult.Content = stream.ToString();
      xmlResult.ContentType = "text/xml";
      return xmlResult;
  }
}
[/sourcecode]
こんな感じでいけるかと思っていたんだけど、JSONは思い通りにできたのに、XMLの方が思ったよりも大変だった。というのは、XmlSerializerがDictionaryのたぐいをシリアライズしてくれないから。

で、[PC][C]C#::DictionaryをXMLSerializerでシリアライズしたいんですが?を見つけて、これでDictionaryをXMLシリアライズしようと思ったんだけど、これでシリアライズすると、

[sourcecode language="xml"] keyname value [/sourcecode]

みたいな感じのXMLとしてシリアライズされる。JSON相当に読みやすい表現の

[sourcecode language="xml"] value [/sourcecode]

になってくれないかなー。というわけで試しに作ってみた。

[sourcecode language="c#"]
    public class SerializableDictionary : Dictionary, IXmlSerializable
    {
        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        string key = null;
        object value = null;
        while (reader.Read())
        {
            switch (reader.NodeType)
            {

                case System.Xml.XmlNodeType.Element:
                    key = reader.Name;
                    break;
                case System.Xml.XmlNodeType.Text:
                    value = reader.Value;
                    break;
                case System.Xml.XmlNodeType.EndElement:
                    if (!string.IsNullOrEmpty(key))
                    {
                        this[key] = value;
                    }
                    else
                    {
                        reader.Read();
                        return;
                    }
                    key = null;
                    value = null;
                    break;
            }
        }
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        foreach (var key in Keys)
        {
            writer.WriteStartElement(key.ToString());
            writer.WriteString(this[key].ToString());
            writer.WriteEndElement();
        }
    }
}

[/sourcecode]

汎用Dictionary<Tkey, Tvalue>の場合の扱い方がよくわからなかったんで、Dictionary<string, object>に限定。あと、valueがさらに階層化されている場合の処理の書き方もわからなかったんで、一次元のDictionaryにしか対応しない。あとNode名としての妥当性チェックとかもしていない。といろいろ課題はあるけど、動く範囲では動く。

これで、最初に書いたようにDictionaryを含むResultオブジェクトを生成しておいて、それをXMLなりJSONなりで出力するようなAPIコードが簡単に書けるようになった。

Published At2011-11-16 21:38Updated At2011-11-16 21:38

技術日記
ASP.NET MVC 3でファイルダウンロードEdit

この前、ASP.NET MVC 3でファイルダウンロードする方法として、ネイティブなResponseオブジェクト処理をすればちゃんとできたという話を書いたけど、もっと圧倒的に簡単な方法があった。

普通のコントローラのアクションメソッドの戻り値では、ViewResultを返してViewでのレンダリング結果を返しているけれども、第3回 ActionResultオブジェクトでアクション操作も自由自在 - @ITにあるとおり、ActionResultにはFilePathResultなんていう、ファイルの内容をそのまま返すActionResultも用意されていた。ってことで、

[sourcecode language="c#"]
string filename = ""; //ダウンロードしたいファイルパス
string downloadname = ""; //  ダウンロードの際に指定したいファイル名
var result = new FilePathResult(filename, "application/octet-stream"); // content-typeは適当にどうぞ
result.FileDownloadName = downloadname;
return result;
[/sourcecode]
なんてアクションメソッド内で書けば、ファイルダウンロード処理が行われる。FileDownloadNameを指定するとContent-Dispositionヘッダがつく。

ASP.NET MVC 3はVisual Studio 2010で自動的にヘルプドキュメント検索が効かないから、こういう便利な機能があちこちに隠れていそうだな。でも、フレームワークのドキュメントを全部読むのはきついからなー。ぼちぼち隙間で読もう。

Published At2011-11-15 20:06Updated At2011-11-15 20:06

技術日記
ASP.NET MVCの認証にMySQL(MySqlMembershipProvider)を使うEdit

MySQL :: MySQL 5.5 Reference Manual :: 21.2.4.2 Tutorial: MySQL Connector/Net ASP.NET Membership and Role Providerが、関連ドキュメント。

ただし、machine.configを云々って説明がよくわからん。web.configのほかにmachine.configを用意して、そっちにMySqlMembershipProviderがらみの設定を書けってこと? なんでweb.configじゃだめなんだ? 設定ファイルを分散させたくないんで、web.configで完結させる方向で試してみる。

まず上記ドキュメントを見ながら、configuration/system.web/membership/providers以下にデフォルトで書かれている、AspNetSqlMembershipProviderキーの内容を以下に変更。

ドキュメントのようにMySqlMembershipProviderというキーで定義しても、デフォルトのAspNetSqlMembershipProviderの方が参照されてしまうので、AspNetSqlMembershipProviderを書き換える方法を選んだ。

たぶんどこかで(machine.config?)デフォルト参照先のプロバイダー名を変更できるんだろうけど、RoleProviderとかTokenRoleProviderとかとの参照関係とかの内容がよくわからないので、追求するのはあきらめた。

[sourcecode language="xml"]

[/sourcecode]
ドキュメント上のサンプルではMySqlMembershipProviderのバージョンがずいぶん古い記述だったので、インストールされているバージョンに合わせた。

続いて、DB接続文字列をここで設定したApplicationServicesという名前で追加。

[sourcecode language="xml"]
    
[/sourcecode]
これで試しにASP.NET MVCアプリケーションのデフォルトのアカウント関連ページに接続すると、必要なテーブル(my_aspnet_*)が自動的に登録されているのが確認できる。

あと、ロール関連のプロバイダーもMySQLのものに変更。

[sourcecode language="xml"]
        
[/sourcecode]
typeで細かいバージョン名まで指定しないと読み込んでくれなかった。

これでASP.NET Webサイト管理ツールを起動したら、ユーザー登録情報の変更やロールの追加などが一通り動くようになった。

続いて、パスワード変更機能を有効化。単にリンクがないだけなんで、Views/Shared/_LogOnPartial.cshtmlのログアウトの後ろに、

[sourcecode language="c#"]
[@Html.ActionLink("パスワード変更", "ChangePassword", "Account")]
[/sourcecode]
を追加。

あと、自由にユーザー登録できるサイトじゃなくしたいんで、AcciontControllerのRegster関連アクションメソッドに[Authorize(Role="superuser")]属性をつけておいて、superuser権限を持つユーザー以外は呼べないようにしておいた。ただ、デフォルトだとユーザー登録したらそのユーザーでログインしてしまうんで、

[sourcecode language="c#"]
FormsAuthentication.SetAuthCookie(model.UserName, false /* createPersistentCookie */);
[/sourcecode]
をコメントアウトして、単なるユーザー登録のみの処理に変更。あとはログイン画面の登録リンクや文言を修正すれば、自由にユーザー登録できないサイトになる。

ちなみにビュー内でユーザー権限チェックなどで条件分岐したい場合は、

[sourcecode language="c#"]
@if (User.IsInRole("superuser")) {
  

superuser向けの表示

} [/sourcecode]
なんて感じでUser経由でアクセスできる。

Published At2011-11-14 17:43Updated At2011-11-14 17:43

技術日記
ADO.NET Entity Frameworkで自動生成されたモデルクラスを拡張する方法Edit

ADO.NET Entity Frameworkで自動生成されたモデルクラスを拡張する方法はあるんだろうか? Foo.edmxに対応するFoo.Designer.csファイルは、Foo.edmxファイルをいじると自動的に上書き再生成されてしまうんで、直接そのファイルを変更することはできない。

でも、データ処理がらみの機能を実装する場合、別クラスを作ってそちらで書くのではなく、モデルクラスの方を直接いじってそちらで拡張した方が直観的な設計になることも多いよね。

で、Foo.Designer.csファイルの中身をよく見てみたら、モデルクラスはpartialで定義されていた。ってことは別ファイルにpartialで追加メソッドを用意すればいいだけなのか。たとえば、barテーブルに対応するBarクラスにメソッドを追加したければ、適当なクラスファイルを用意して、

[sourcecode language="c#"]
public partial class Bar : EntityObject
{
    public string ExtendedMethod()
    {
        return "extended!";
    }
}
[/sourcecode]
とかでOK。

これで機能拡張はできるとして、標準的な操作をoverrideしたりはできないのかな? たとえばinsert、update時にはcreated_at、modified_atとかを自動で更新したりとか。

調べてみたところ、EntityObjectにはPropertyChangedというイベントがあるので、それをフックすれば良さそう。

[sourcecode language="c#"]
public partial class Bar: EntityObject
{
    public Bar() : Base()
    {
        PropertyChanged += OnPropertyChanged;
    }

    protected void OnPropertyChanged(object sender, PropertyChangedEventArgs e)     {         if (new string[]{"updated_at", "created_at"}.Contains(e.PropertyName)) return;         if (id == 0) {// primary keyがセットされていない=新規作成             created_at = DateTime.Now;         }         updated_at = DateTime.Now;  } } [/sourcecode]

一応なんてコードでそれっぽく動いている。新規作成 or 更新の区別をid == 0かどうかで識別しているのが妥当かどうかはちょっと怪しい。ほかにもっとちゃんとした方法はないかな。

EntityStateってのを使うとそれっぽいことができそう。オブジェクトの作成・更新・削除状態が取得できるプロパティの模様。と思ったが、これはオブジェクトコンテキストにAttachされていないと状態が取得できないのか。

EntityObjectインスタンスメソッドのスコープからは親オブジェクトコンテキストが見えないから、いったんAttachして確認することもできないし、副作用もありそうだからあまりやりたくもないなー。ひとまずはidを見て切り分けることにしておくか。

Published At2011-11-11 21:40Updated At2011-11-11 21:40

技術日記
ファイルがない場合だけ動的生成するCMSEdit

思いつきアイディア。mod_rewriteを使って、ファイルが存在したらそのファイル自体をそのまま(httpdのレベルで)返し、なければWebアプリケーションを呼ぶという仕組みはよくある。

呼ばれた先のWebアプリケーションで、リクエストに応じた処理を行って結果を返すというのが動的生成。静的生成は、管理ツールなんかでコンテンツを投稿したら、その時点でHTMLファイルとかを生成して、Web公開ディレクトリに置いておく。

その中間として、コンテンツが投稿された時点ではHTMLファイルは存在せず、アクセスされた時にオンタイムで動的生成して、そのファイルをWeb公開ディレクトリに直接ファイルとして保存してしまう。

仕組み的にはコンテンツキャッシュに似ているけど、キャッシュではなくて実コンテンツファイルを初回アクセス時に生成する仕組み。

アクセスのないコンテンツは定期的に削除してしまってディスク使用量を削減したり(今時あまり意味ないだろうけど)、すべてのコンテンツに影響がある設定変更などを行った場合は、すべてのコンテンツを削除してしまうだけで、またアクセスがあったときに自動生成される。

フロントのWebサーバーを追加したりするときは、コンテンツファイルの全コピーを行わなくても、そのサーバーにアクセスがあったときに必要なファイルが生成されるので、DBサーバーの負荷が適当に分散される。 昔MovableTypeとかで全リビルドとかすると結構大変だったし。

といった感じの仕組み。すでにどこかにあるかな?

Published At2011-11-11 16:24Updated At2011-11-11 16:24

技術日記
小型家電・PC周辺機器向けバッテリーパック規格Edit

Twitterでつぶやいたことをまとめてみる。

震災直後の電力供給不安定があったり、家庭用のNASやルーター、ハードディスクレコーダーなどの常時通電しておきたい家電やPC周辺機器が増えてきていて、ブレーカーが落ちたりすると面倒くさいことになったりする。

でも、無停電電源装置(UPS)を導入するのは面倒くさい。でかくて重くて入手性が悪くてバッテリー寿命が比較的短い割には交換するのが面倒くさい。

そこで、常時通電しておきたい家電やPC周辺機器向けに、簡易UPS的な機能を持たせるための乾電池なんかよりももっと大容量のバッテリーパック規格を用意し、それを使ってUPS的な機能が簡単に追加できるようにならないか。

バッテリーはそれほど長持ちする必要はなく、瞬間停電に耐えられたり、あるいは正常に終了プロセスを実行できる程度の時間持てば十分。その規格が普及して、交換バッテリーパックがコンビニとかでも気軽に入手できるようになったら、非常に便利なのだが。

Published At2011-11-11 16:01Updated At2011-11-11 16:01

技術日記
技術調査記録Edit

ASP.NET MVCで独自の(ビューの)ヘルパー(メソッド)を追加したい。ググると出てきたのが、[C#] #13. カスタム HTML ヘルパーの作成 | ASP.NET MVC のチュートリアル。2010年5月20日のドキュメントだから、賞味期限的にも大丈夫そう。

このドキュメントを見る限りは、ヘルパーといっても基本的には単に文字列を返すC#のスタティックメソッドをImport Namespaceでビューから見えるようにしてやっているだけか。

サンプル コード 5 のクラスは、Label() という名前の HtmlHelper クラスに拡張メソッドを追加します
とあるけど、これは「サンプルコード5(のクラス)は、HtmlHelperクラスにLabel()という名前の拡張メソッドを追加します」だよな。

既存のHtmlHelperクラスに拡張メソッドを追加するために、LabelExtensionsというスタティッククラスを定義し、その中のLabel()メソッドの第1パラメータをthis HtmlHelper helperとすることで、そのメソッドが既存のHtmlHelperクラスの拡張メソッドであることを宣言しているから、最初のサンプルでは、

[sourcecode language="c#"]
<%@ Import Namespace="MvcApplication1.Helpers" %>
<%=@LabelHelper.Label("firstName", "First Name: ") %> 
[/sourcecode]
みたいに書いていたものを、
[sourcecode language="c#"]
<%@ Import Namespace="MvcApplication1.Helpers" %>
<%=Html.Label("firstName", "First Name: ") %>
[/sourcecode]
みたいに書けるよってことだね。

ちょっとした拡張ならば後者の方が良さそうなんで、後者の方法で書いてみることにする。

[sourcecode language="c#"]
using System.Web.Mvc;

namespace MyApp.Helpers { public static class HelperExtensions { public static string Summarize(this HtmlHelper helper, string text, int length = 80, string suffix = "..") { if (text.Length <= length) { return text; } string result = text.Substring(0, length - suffix.Length) + suffix; return result; } } } [/sourcecode]

こんな感じで、@Html.Summarize(text)とかすると、80文字以上の長いテキストを切り詰めて表示してくれる。

ちなみにRazorでの<%@ Import Namespace = "MyApp.Helpers" %>相当の命令は、単純に@using MyApp.Helpers;でよかった。

続いて、通常HTML表示している内容を、API向けにXMLで出力する方法。XMLはテンプレートで出力するんで、IndexXml.cshtmlとかXML出力用のテンプレートを用意し、アクションメソッドの中で、

[sourcecode language="c#"]
if (format == "xml") {
  Response.ContentType = "text/xml";
  return View("IndexXml", data);
}
[/sourcecode]
みたいな感じにした。テンプレートは元々PHPで書いていたものをコピーしてきて、Razor向けに書き換え。PHPで書いたテンプレートをRazorで書き直したら、Razorの方がだいぶ書きやすかった。型付けされたモデルクラスのおかげでVisualStudio上で自動補完も効くし。

これでOKかと思いきや。実際にはだめ。というのはよく考えたらレイアウトファイル(マスターページ)が適用されてしまうから、XMLの前後にHTMLのレイアウトページが出力されてしまう。レイアウト出力の抑制ってどう書くんだ?

ひとまずテンプレートファイルの中で、@{Layout = null;}をしておくと、レイアウトの抑制はできた。けど、アクションメソッドの方でコントロールしたいな。

一応ViewResultのオプションでマスターページ名を指定できるみたいなんで、

[sourcecode language="c#"]
return View("IndexXml", null, data);
[/sourcecode]
とかやってみたんだけど、デフォルトのレイアウトが適用されてしまう。テンプレートの中でLayout = null;はいけるのに、マスターページとしてnullを渡したんじゃだめなのか。しょうがないから、_LayoutNothing.cshtmlとかいう名前で、
[sourcecode language="c#"]
@RenderBody();
[/sourcecode]
みたいに何もしないレイアウトファイルを用意して、
[sourcecode language="c#"]
return View("IndexXml", "_LayoutNothing", data);
[/sourcecode]
としたら一応思い通りに動いた。なんか今ひとつ釈然としないけれども、まあいいか。

ちなみにこの辺を調べるためにドキュメントをあさっていたら、System.Mvc以下にJsonResultというAPI向けにJSON出力するためのクラスが用意されていた。これってXML出力用は用意されていないの? XML出力したい場合は通常のシリアライザー使って書けってことなのかな?

Published At2011-11-10 20:39Updated At2011-11-10 20:39