説明
筆者はASP.NET MVCで、コントローラ内でContentResult
を利用したHTMLスニペットの生成アクションを作成していた。
一からHTMLを起こすのは大変なので、Viewで使われているHtmlHelper
クラスが使えればうれしいのだが、使えるように設定するのは至難の業だった。
問題その1: 情報が古い
この件を検索して出てきた情報がこちらだが、12年前の投稿で、いつの間にかHtmlHelper
コンストラクタの引数が変更されていた1。WebFormView
コンストラクタの引数にControllerContext
が追加されたのだ。新しい引数に対応したコンストラクタはこちらに載っていた…と思ったら、こちらも10年前なので古い情報だった1。今度はViewContext
コンストラクタの引数にTextWriter
が追加された。そこで引数を調べたのだが…
問題その2: 情報が少ない
各引数の型(TextWriter除く)はASP.NET内部で使用されることが前提のため、公式でもそれほどドキュメントが準備されておらず、かと言ってネットを検索してもそれらに関する情報がほとんど無かった。
正解
そんな四苦八苦の末、たどり着いた結論は、2番目のリンクの解決策を少し修正したものとなった。
public ContentResult Baz() {
var helper = new HtmlHelper<FooModel>(new ViewContext(ControllerContext, new WebFormView(ControllerContext, "Nemo"), new ViewDataDictionary(), new TempDataDictionary(), new StringWriter()), new ViewPage()); //この行!
var textFor = helper.TextFor(m => m.Bar);
var htmlCode = GenerateHtml(textFor) // textForをhtmlコードに挿入する。詳細は割愛する。
return Content(htmlCode , "text/html");
}
TextWriter
は抽象クラスのため、実装の1つであるStringWriter
を利用する。WebFormView
のコンストラクタの2つ目の引数は識別子みたいなものらしく、名前は適当でよい(ただし空文字はNG)。
最後に―ContentResult
の戻り値について―
少なくともChromeの場合(FirefoxやSafariは環境に未インストールのため検証不可)、ContentResult
でHTMLスニペットをそのまま返してしまうと、受け取り側(ブラウザのJavaScript)で不正なHTMLとみなされてしまうのか、HTMLの書式に合わない部分は省略されてしまう2。この辺りは<template>
要素を使用した場合と事情が異なっている。
解決はやや手間がかかるが簡単で、HTMLスニペットを妥当なHTML文書に格納し、そこから必要な要素を読みだせばよい。先ほどのコード中のGenerateHtml
はそれをするためのメソッドだが、実装の仕方は人それぞれだろうから中身は省略した。後々のことを考えると、Compositeパターンを取り入れて、タグを入れ子にできるTagBuilder
の上位互換クラスを作成するのが良いのではないか。
English Version Here
I was making Action
inside the controller for generating HTML snippets as ContentResult
using ASP.NET MVC.
I'd like to use HtmlHelper
since writing HTML from the ground up is tedious, but how to use it was a trek.
Probrem 1: Information is old
The information I came across when I searched the issue is here. However, this info was published 12 years ago and HtmlHelper
constructor has changed; ControllerContext
was added to the arguments of the WebFormView
constructor. I found newer solution, though that was also 10 years old one and it lacks one of the ViewContext
's argument that is TextWriter
. Also, they are both aren't generic, so they can't use methods such as EditorFor()
, which needs the information of type the model uses.
Probrem 2: Information is scarce
Most of the arguments' type are supporsed to be used internally, so there are not much documents even officially, and there are few piecies of information around the web.
The solution
I managed to implement the helper. The idea was basically from the second link.
public ContentResult Baz() {
var helper = new HtmlHelper<FooModel>(new ViewContext(ControllerContext, new WebFormView(ControllerContext, "Nemo"), new ViewDataDictionary(), new TempDataDictionary(), new StringWriter()), new ViewPage()); // This line!
var textFor = helper.TextFor(m => m.Bar);
var htmlCode = GenerateHtml(textFor) // Inserts textFor to html code. Details are omitted here.
return Content(htmlCode , "text/html");
}
Because the TextWriter
is a abstruct class, I needed to use the StringWriter
class, which is one of the extension of it. The second argument of the WebFormView
constructor seems to be an identifier, thus the name can be anything but empty.
At last - the return value of ContentResult
At reast in Chrome (I can't verify Firefox or Safari now), if the ContentResult
returns raw HTML snippets, receiver (the JavaScript of the browser) appears to interpret the HTML as malformed; The parts that's not corresponding HTML rule3 is omitted. This behavior is different from using the <template>
elements. The answer is simple yet needs some work - Put the snippet into a valid HTML and JS reads the necessary part. GenerateHtml
in the previous code example is for the purpose, but how to implement it is your problem. I suggest making the upper conversion of the TagBuilder
class that can insert tags inside using the Composite Pattern. It would be useful in the future.