しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

JsonValueProviderFactory と JsonResult の XML 版を作ってみた

「今更 XML かよ」とか「時代は JSON だろjk」という声がいろんなところから聞こえてきそうですね。実際自分でも「誰得だよ…」とか思いました。

しかし id:ch3cooh393:20110116:1295196338 を読んだ時にひらめいてしまったので勢いで作ってしまいました。まあ、作ったといっても MVC 3 の JsonValueProviderFactory を改造しただけです…。Ms-PL だから大丈夫!

それでは XmlValueProviderFactory のソースです。配列と連想配列の扱いを非常に迷ったのですが、今回は全て連想配列として扱うことにしたので配列は上手くバインドできないはずです。

public sealed class XmlValueProviderFactory : ValueProviderFactory
{
    private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, XElement value)
    {
        if (value.HasElements)
        {
            foreach (var entry in value.Elements())
            {
                AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Name.LocalName), entry);
            }
            return;
        }

        backingStore[prefix] = value.Value;
    }

    private static XElement GetDeserializedObject(ControllerContext controllerContext)
    {
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/xml", StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
        var bodyText = reader.ReadToEnd();
        if (String.IsNullOrEmpty(bodyText))
        {
            return null;
        }

        return XElement.Parse(bodyText);
    }

    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }

        var xmlData = GetDeserializedObject(controllerContext);
        if (xmlData == null)
        {
            return null;
        }

        var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        AddToBackingStore(backingStore, String.Empty, xmlData);
        return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
    }

    private static string MakePropertyKey(string prefix, string propertyName)
    {
        return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
    }
}

モデルバインダがどのような仕組みになっているのか知らなかったのですが、DictionaryValueProvider は Dictionary から上手いことオブジェクトにバインドしてくれるナイスなクラスみたいです。

処理の内容としては AddToBackingStore メソッドを再帰的に呼び出して XML 全体を走査します。AddToBackingStore では XAML でいうプロパティ式をキーとしたディクショナリを作ってしまえば、あとは DictionaryValueProvider が頑張ってくれます。

バインドが出来れば ActionResult で XML 返せなきゃね。という感じだったので XmlResult を作りました。中身は XLINQ なら ToString、それ以外なら XmlSerializer を使っているだけのシンプルな実装です。

public class XmlResult : ActionResult
{
    public Encoding ContentEncoding { get; set; }

    public string ContentType { get; set; }

    public object Data { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        HttpResponseBase response = context.HttpContext.Response;

        if (!String.IsNullOrEmpty(ContentType))
        {
            response.ContentType = ContentType;
        }
        else
        {
            response.ContentType = "application/xml";
        }
        if (ContentEncoding != null)
        {
            response.ContentEncoding = ContentEncoding;
        }
        if (Data != null)
        {
            if (Data is XNode)
            {
                response.Write(((XNode)Data).ToString());
            }
            else
            {
                var xs = new XmlSerializer(Data.GetType());
                xs.Serialize(response.Output, Data);
            }
        }
    }
}

そして「XmlResult が出来たらヘルパーメソッドが無いと使う気がしないよー」という感じだったので、ヘルパーメソッドを作りました。Controller の拡張メソッドでもいいのですが、僕は this が付くの嫌いなので protected メソッドで定義します。

protected XmlResult Xml(object data)
{
    return Xml(data, null, null);
}

protected XmlResult Xml(object data, string contentType)
{
    return Xml(data, contentType, null);
}

protected XmlResult Xml(object data, string contentType, Encoding contentEncoding)
{
    return new XmlResult
    {
        Data = data,
        ContentType = contentType,
        ContentEncoding = contentEncoding,
    };
}

これで Json と同じように XML も使えるようになりましたね!でも XML を JavaScript で作るのは非常にめんどくさいので、API として公開した方が良いんでしょうねー。でもそれなら WCF とか使った方が良いんじゃないの??でオワタ状態です。

とりあえず動作確認を行っておきましょう。JsonValueProviderFactory とほぼ同じなので、兄者の「ASP.NET MVC3 RC2のJsonValueProviderFactoryを利用して非同期処理を試す! - 割と普通なブログ」記事をまるっと使わせてもらいました。

変更点ですが、JavaScript 側は $.ajax を呼び出すパラメータが異なってます。

$.ajax({
    type: "POST",
    dataType: "xml",
    contentType: 'application/xml',
    url: '/Home/Create',
    data: '<person><age>9</age><name>高町 なのは</name><comment>リリカルマジカル</comment></person>',
    success: function (serverResponse) {
        //サーバサイドから受け取ったXML形式レスポンスデータを表示する
        $('#message').text(serverResponse.xml);
    },
    error: function (e) {
        alert('エラー終了です');
    }
});

アクションは非常にシンプルで受け取ったデータをそのまま返しているだけです。ちゃんとバインド出来ているか、オブジェクトを XML として出力できるかだけを見たいのです。

[HttpPost]
public ActionResult Create(Person person)
{
    return Xml(person);
}

大事なことを忘れていました、作成した XmlValueProviderFactory ですがちゃんとフレームワークに登録しないと使って貰えません。非常に当たり前のことですね。

Factory の登録は Global.asax の Application_Start で書くと決まっています。

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

    // 作った Factory を登録する
    ValueProviderFactories.Factories.Add(new XmlValueProviderFactory());

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

それでは実際にブラウザで確認してみましょう。

POST したのと同じデータが XML 形式で表示されているのが確認できます。つまりバインドと出力の両方がうまくいっていることが確認できました。

もうちょっと ValueProvider を真面目に書けば使い物になるのかもしれないですねー。案外 XML-RPC に特化した方が良いのかも??*1

*1:でも、それならますます WCF とか使えという話に…。