1.21 jigowatts

Great Scott!

ASP.NET MVC オートコンプリート

概要

あると便利!jQuery UIのAutocomplete(オートコンプリート)をASP.NET MVCで実装してみます。
今回のサンプルは入力した文字列に応じて入力候補が表示され、選択した値が各項目にセットされるというものです。

f:id:sh_yoshida:20160612013611g:plain

環境

Visual Studio Community 2015 Update 2
ASP.NET MVC5
jQuery UI

実装

コントローラー

必要なのはJSONデータなので、JsonメソッドによりJsonResultオブジェクトを返します。
既定ではHTTP GETリクエストではJSONデータを返せないようです。
Controllers\HomeController.cs

//ビューの表示
public ActionResult Create()
{
    return View();
}

public JsonResult AutoComplete(String name)
{
    var data = _service.GetPeopleByName(name);
    return Json(data);
}

明示的にGETリクエストを許可するには以下のようにする必要があります。

return Json(data, JsonRequestBehavior.AllowGet);

ワーカーサービス

多くの場合、リポジトリクラスからデータベースにアクセスし必要なデータを取得すると思いますが、今回はサンプルのため擬似コードで割愛。
Services\People\PeopleService.cs

public class PeopleService : IPeopleService
{
    public IEnumerable<Person> GetPeopleByName(string name)
    {
        var data = GetData();
        var result = data.Where(c => c.Name.StartsWith(name, ignoreCase: true, culture: ci));

        return result;
    }
}

モデル

Personクラスをベースにします。
Models\Person.cs

public class Person
{
    public int Id { get; set; }
    public String Name { get; set; }
    public String Email { get; set; }
    public String Birthday { get; set; }
}

ビュー

簡単な入力フォーム上でName項目に入力した値で動作するようにしています。
Views\Home\Create.cshtml

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Person</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Birthday, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Birthday, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Birthday, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

JavaScript

$(function () {
    $('#Name').autocomplete({
        source: function (request, response) {
            var param = { name: $('#Name').val() };

            $.ajax({
                url: '@Url.Content("~/Home/AutoComplete")',
                data: JSON.stringify(param),
                datatype: "json",
                type: "POST",
                contentType: "application/json",
                dataFilter: function (data, type) { return data },
                success: function (data, dataType) {
                    response($.map(data, function (item) {
                        return {
                            label: "ID:" + item.Id + " Name:" + item.Name + " Email:" + item.Email + " Birthday:" + item.Birthday,
                            value: item.Name,
                            extend: item
                        };
                    }));
                },
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    alert('error');
                }
            });
        },
        select: function (event, ui) {
            $('#Email').val(ui.item.extend.Email);
            $('#Birthday').val(ui.item.extend.Birthday);

        }
    });
});

実行結果

Name項目に入力した文字列に対して入力候補が絞られていきます。
最終的に選択した値が各項目にセットされました!
f:id:sh_yoshida:20160612013611g:plain

ASP.NET Web APIという選択

ASP.NET MVCはRPCベースのスタイルであるのに対し、ASP.NET Web APIはRESTfullスタイルが既定(RPCもイケル)です。
ASP.NET MVCではJSONデータが返せるため、あまりメリットを感じないのですが、本来はWeb APIとして提供した方がいいケースもあります。

上記のコードを少し変更しつつ、ASP.NET Web APIバージョンも実装してみます。

実装

APIコントローラー

MVC6では統合されたようですが、今回はMVC5なのでASP.NET Web APIコントローラーはまだSystem.Web.HttpアセンブリのApiControllerを継承しています。
Api\PeopleController.cs

public class PeopleController : ApiController
{
    public IEnumerable<Person> GetByName(String name)
    {
        return _service.GetPeopleByName(name);
    }
}

プロジェクト作成時にテンプレートでWeb APIを選択していない場合、APIコントローラーを追加しただけでは動かないため、以下を追加します。

既定のルートの設定

App_Start\WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}
Web APIの既定ルートを登録

Global.asax

protected void Application_Start()
{
    //追記
    GlobalConfiguration.Configure(WebApiConfig.Register);
    ...
}

ビュー

GETリクエストでhttp://{server}/api/{apicontroller}へアクセスしJSONデータを取得します。
JavaScript

$(function () {
    $('#Name').autocomplete({
        source: function (request, response) {
            var param = { name: $('#Name').val() };

            $.getJSON('@Url.Content("~/api/people")', param)
            .done(function (data) {
                response($.map(data, function (item) {
                    return {
                        label: "ID:" + item.Id + " Name:" + item.Name + " Email:" + item.Email + " Birthday:" + item.Birthday,
                        value: item.Name,
                        extend: item
                    };
                }));
            })
            .fail(function (jqXHR, textStatus, err) {
                alert('error : ' + err);
            });
        },
        select: function (event, ui) {
            $('#Email').val(ui.item.extend.Email);
            $('#Birthday').val(ui.item.extend.Birthday);
        }
    });
});

まとめ

見た目は同じですが、実装をASP.NET Web APIに変更してみました。
どちらを選択するかは仕様によると思いますが、MVC6で統合された経緯を見る限りどちらも使いこなせたほうが良さそうですね。
ASP.NET Coreを視野に入れつつASP.NET Web APIも学習してみようっと。

参考

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版

.NET開発テクノロジ入門 2014年版 VisualStudio2013対応版 (MSDNプログラミングシリーズ)

.NET開発テクノロジ入門 2014年版 VisualStudio2013対応版 (MSDNプログラミングシリーズ)

ASP.NET MVC ファイルアップロード

概要

久しぶりにASP.NET MVCです。以前いくつかMVC2についてポストしましたが、いい加減Razor使いたい!ということでASP.NET MVC5にしてみました。まったく仕事でMVC5を使う予定はありませんがね!

それはさておき、ファイルのアップロードです。最初に完成イメージ。
f:id:sh_yoshida:20160607211237p:plain

環境

Visual Studio Community 2015 Update 2
ASP.NET MVC5

実装

アップロードする画面と結果を表示する画面に対応したコントローラー、モデル、ビューで構成されています。

コントローラー

ファイルをアップロード後にファイル一覧を表示する画面にリダイレクトしています。
Controllers\HomeController.cs

[HttpGet]
public ActionResult Upload()
{
    return View();
}

[HttpPost]
public ActionResult Upload(UserData inputModel)
{
    var destinationFolder = Server.MapPath("~/Users");

    if (!Directory.Exists(destinationFolder))
    {
        Directory.CreateDirectory(destinationFolder);
    }

    var postedFile = inputModel.Picture;
    if (postedFile.ContentLength > 0)
    {
        var fileName = Path.GetFileName(postedFile.FileName);
        var path = Path.Combine(destinationFolder, fileName);

        postedFile.SaveAs(path);
    }

    return RedirectToAction("FileIndex", "Home");
}

[HttpGet]
public ActionResult FileIndex()
{
    var destinationFolder = Server.MapPath("~/Users");
    string[] files = Directory.GetFiles(destinationFolder, "*", SearchOption.AllDirectories);
    ViewBag.files = files;

    return View();
}

モデル

アップロードファイルのモデルバインディングに対応するにはHttpPostedFileBase型として宣言する必要があります。
Models\UserData.cs

public class UserData
{
    public string Name { get; set; }
    public string Email { get; set; }
    public HttpPostedFileBase Picture { get; set; }
}

ビュー

(念願のRazorです!)
ファイルアップロードするにはformタグにenctype = "multipart/form-data"が必要です。
Views\Home\Upload.cshtml

@using (Html.BeginForm("Upload", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>UserData</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Picture, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <input type="file" id="picture" name="picture" class="form-control" />
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

結果はViewBagに入れて表示するだけ。
Views\Home\FileIndex.cshtml

@foreach (var file in ViewBag.files)
{
    <div>@file</div>
}

実行結果

Pictureを選択して[Create]ボタンを押下すると…

f:id:sh_yoshida:20160607211237p:plain

保存されました!これはAzure App ServiceのWebサイトにアップした結果です。こんなパスになるんですね。

f:id:sh_yoshida:20160607231524p:plain

Kuduでも確認してみました(初めて使ってみる)。ちゃんと保存されているようです。

f:id:sh_yoshida:20160607231532p:plain

実際にはセキュリティだとかエラーハンドリングだとかもっとしっかりやらないとダメですよ!(‘А`)

Server.MapPathについて

参考書には以下のように指定されてまして、この通りでもローカルで実行した場合はうまく動きました。

Server.MapPath("/Users")

ただ、IISにデプロイするときにアプリケーション名(このサンプルではvoyager02)を指定するとパスが通らなくなってしまいました。
f:id:sh_yoshida:20160607235039p:plain

これは/(スラッシュ)を指定するとWebサイト(ドメインのルート)の物理パスを取得するため一個上の階層になってるんですね。また、ドメインのルートとアプリケーションのルートは必ずしも階層関係にある必要はないため余計にややこしいですね。

f:id:sh_yoshida:20160607235053p:plain

Webサイト直下に配置するのであればスラッシュでもいいのかもしれませんが、ルート演算子(~)をつけてあげるとアプリケーションルートの物理パスが取れるので、アプリケーション名に依存しなくなります。

Server.MapPath("~/Users")

参考

stackoverflow.com

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版

ASP.NET MVC実践してみました~あれ!System.Web.Mvc.dllがない!?

概要

ASP.NET MVCを仕事で使いたく独学でコソコソやってましたが、手ごろなプロジェクトがあったので実践してみました。ざっくりな仕様としては、データベースのデータを取ってきて表示するだけの簡単なWebアプリケーションです。
開発フェーズも中盤に差し掛かり、検証サーバで動作確認をしたところ、ブラウザが「System.Web.Mvcが見つかりません」とエラーメッセージを吐きました。調べてみると検証サーバにはASP.NET MVCランタイムがインストールされていませんでした/(^o^)\

環境

Visual Studio 2010
ASP.NET MVC2
SQLServer2008 R2
.NET Framework 4

詳細

基本的な実装を行いローカルでテスト、開発サーバにデプロイして動作確認をクリアし、検証サーバにデプロイしたら動きません!
ブラウザのエラーメッセージは次の通り。

パーサー エラー メッセージ: ファイルまたはアセンブリ ‘System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35’、またはその依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。

これって、ASP.NET MVCアプリケーションが動作するためには、System.Web.Mvc.dllというアセンブリが必要なんだけど、ないよってことですよね。
開発サーバと検証サーバの環境が違うだとッ!(真顔

原因

VisualStudio2010にはASP.NET MVC2がインストールされていますが、.NET Framework4にASP.NET MVCランタイムは含まれません。(なんだって!?
ソース:http://blogs.msdn.com/b/chack/archive/2010/06/14/asp-net-mvc-2-deployment-on-iis-7-5.aspx

ローカルはもちろん、開発サーバにもVisual Studioがインストールされていることにより、ASP.NET MVCランタイムがインストールされており、実行可能でした。
ところが、検証サーバ(本番も)上にはVisual Studioはもちろんとして、ASP.NET MVCランタイムがインストールされていませんでした。(あばばばば
.NET Framework4なら動くもんだと思ってましたよ。

解決

Microsoft Web開発ガイドラインには次のように書かれていました。
http://download.microsoft.com/download/7/C/E/7CE5CC56-F897-42B0-9E49-7301B451AE87/WebDevelopmentGuideline_Rev1.pdf

ASP.NET MVCをインストールすると、次のアセンブリがコンピュータ上のグローバルアセンブリキャッシュ(GAC)に配置されます。

ホスティングプロバイダがASP.NETバージョン3.5Server Pack1をインストールしている場合、アップロードする必要があるのはSystem.Web.Mvcアセンブリだけです。
Microsoft Web開発ガイドライン P.198より

今回の件では、都合上サーバ側はまったく触れないのでASP.NET MVCランタイムをインストールすることはできません。このため、必要なアセンブリをアップロードします。

アセンブリロード時は基本的に以下の2か所を検索するようです。

  1. グローバルアセンブリキャッシュ(GAC)
  2. アプリケーションフォルダ(bin)

GACにSystem.Web.Mvc.dllが存在しないので、パッケージビルド時に対象のアセンブリがローカルコピーされるよう設定します。

[参照設定]で”System.Web.Mvc”を選択し、[プロパティ]にてCopy LocalをTrueに変更します。
f:id:sh_yoshida:20150311155917p:plain


これによりbin展開されコードベースの検索の結果、アプリケーションが動作するようになります。

まとめ

ASP.NET MVCで開発したいという逸る気持ちが開発環境と検証環境は同じだと過程してしまい、開発環境のみの検証でことを進めたのがそもそもの過ちです。実績のないテクノロジーを投入する場合、入念な検証が必要なことを身をもって知りました。今回はなんとか動きましたが、全部Webフォームで作り直しになるかと...。新しい技術を試させてくれと言っておいて動きませんじゃシャレになりませんよね。ペロ

次からは検証サーバも含めて導入の検討と、検証サーバへの早期デプロイ(できればCIも)を実践するゾ☆