KatsuYuzuのブログ

.NET の開発をメインとした日記です。

ASP.NET MVC 5 でクライアントライブラリの管理を nuget から bower + grunt-bower-task に移行した #aspnetjp

VisualStudio 勢として高見の見物していたライブラリ管理*1だったけど、.NET 周りはいいとして JS, CSS 周りは NuGet に限界を感じ始めたので移行しました。
まだまだ全然わかんないんだけど、とりあえず触ってみたとこ。なので説明は省いて手順だけ。

× MVC 4、○ MVC 5(2016/01/20 追記)

ASP.NET 5、ASP.NET MVC 6 と色々なバージョンが頭でごっちゃになって間違ってました。てへ

ASP.NET MVC 5 でクライアントライブラリの管理を nuget から bower + grunt-bower-task に移行した

f:id:KatsuYuzu:20160120000038p:plain

環境

目標

  • ASP.NET Web アプリケーション MVC 5 テンプレート を踏襲したファイル配置。
    • ASP.NET MVC 6 や業界標準は(わからないから)考えないこととする。
  • angularjs を使った簡単な Web サイトを作るためのライブラリ準備
  • bootstrap には日本語も美しく表示できるテーマ「honoka」から派生した「umi」を採用

手順

  1. NuGet で管理している JS, CSS をアンインストール
  2. package.json を記述(npm)
  3. bower.json を記述(bower)
  4. gruntfile.js を記述(grunt)
  5. タスクランナーで実行

NuGet で管理している JS, CSS をアンインストール

  • packages.config から記述を消して、Scripts\_references.js ファイル以外の Scripts, Contents, fonts フォルダー以下のファイルを削除
    • bootstrap
    • jQuery
    • jQuery.Validation
    • Microsoft.jQuery.Unobtrusive.Validation
    • Modernizr
    • Respond

package.json を記述(npm)

  • [プロジェクトを右クリック] > [追加] > [新しい項目] > [package.json]
{
  "version": "0.0.0",
  "name": "example",
  "private": true,
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-bower-task": "~0.4.0"
  }
}

bower.json を記述(bower)

  • [プロジェクトを右クリック] > [追加] > [新しい項目] > [bower.json]
{
  "name": "example",
  "private": true,
  "dependencies": {
    "jquery": "2.2.0",
    "bootstrap-sass": "3.3.6",
    "Umi": "3.3.6-1",
    "angular": "1.4.8",
    "angular-messages": "1.4.8",
    "angular-route": "1.4.8",
    "angular-loading-bar": "0.8.0",
    "angular-hotkeys": "chieffancypants/angular-hotkeys#~1.6.0"
  },
  "devDependencies": {
    "DefinitelyTyped": "https://github.com/borisyankov/DefinitelyTyped.git"
  },
  "exportsOverride": {
    "jquery": {
      "js": "dist/*.*"
    },
    "bootstrap-sass": {
      "bootstrap.scss": "assets/stylesheets/bootstrap",
      "bootstrap.fonts": "assets/fonts/bootstrap/*.*",
      "js": "assets/javascripts/bootstrap.*"
    },
    "Umi": {
      "umi": "scss"
    },
    "angular": {
      "css": "angular-csp.css",
      "js": "angular.*"
    },
    "angular-messages": {
      "js": "angular-messages.*"
    },
    "angular-route": {
      "js": "angular-route.*"
    },
    "angular-loading-bar": {
      "css": "build/*.css",
      "js": "build/*.js"
    },
    "angular-hotkeys": {
      "css": "build/*.css",
      "js": "build/*.js"
    },
    "DefinitelyTyped": {
      "ts": [
        "jquery/*.d.ts",
        "bootstrap/*.d.ts",
        "angularjs/*.d.ts",
        "angular-loading-bar/*.d.ts",
        "angular-hotkeys/*.d.ts"
      ]
    }
  }
}

TypeScripts の型定義「DefinitelyTyped」はすべてのライブラリ分を DL してくる。横着しすぎでしょ感あるけど実際楽!

gruntfile.js を記述(grunt)

  • [プロジェクトを右クリック] > [追加] > [新しい項目] > [gruntfile.js]
module.exports = function (grunt) {
    grunt.initConfig({
        bower: {
            install: {
                options: {
                    targetDir: "",
                    cleanTargetDir: false,
                    layout: function (type, component, source) {
                        if (type === "js") {
                            return "Scripts";
                        } else if (type === "ts") {
                            return "Scripts/typings/" + source.match(/\\[^\\]*(?=\\[^\\]*$)/)[0].replace("\\", "");
                        } else if (type === "css") {
                            return "Content";
                        } else if (type === "umi") {
                            return "Content";
                        } else if (type === "bootstrap.scss") {
                            return "Content/honoka/bootstrap";
                        } else if (type === "bootstrap.fonts") {
                            return "fonts";
                        } else {
                            return "__untyped__";
                        }
                    }
                }
            }
        }
    });

    grunt.registerTask("default", ["bower:install"]);
    grunt.loadNpmTasks("grunt-bower-task");
};

タスクランナーで実行

  • [表示] > [その他のウィンドウ] > [タスク ランナー エクスプローラー] > [bower:install]

f:id:KatsuYuzu:20160120000038p:plain
f:id:KatsuYuzu:20160120001012p:plain
f:id:KatsuYuzu:20160120001337p:plain

*1:Web 業界が色々なツールとそれぞれの config.json 書いてるのが正直わからない

カスタムモデルバインダーのすゝめ #aspnetjp

この記事はASP.NET Advent Calendar 2015 - Qiitaに参加しています。17日の担当です。前日は id:minato128 さんでした。

カスタムモデルバインダーのすゝめ

f:id:KatsuYuzu:20151217023340p:plain
ASP.NET MVCではモデルバインダーという仕組みで、リクエストの値をモデルに紐づけてくれます。自分でFormの中身やクエリーストリングを見たり、パースしたりしなくてもいい素晴らしい仕組みです。お手軽さ、直感さ、大好き。
そして、もちろんカスタマイズできます。和暦 "H27/12/17" を DateTime 型にしたいなんて闇もモデルバインダーで済ませられれば捗ります。

_________________________ 
    <○√ 
     ∥    ←モデルバインダー
      くく 

作成方法

  1. DefaultModelBinder を継承
  2. BindModel をオーバーライド
  3. カスタムモデルバインダーの登録

DefaultModelBinder を継承 ~ BindModel をオーバーライド

DefaultModelBinder を継承して BindModel をオーバーライドするだけです。その中でモデルステート用の設定を呼び出す等の多少のお作法がある程度。
今回は "1,000" というカンマ区切りの数値文字列を数値型で受け取れるようにしてみます。*1

DefaultModelBinder を継承

数値型の部分をジェネリックにして、型制約を適当に。*2

/// <summary>
/// 1000 の桁で区切られた数字のブラウザー要求を数値型に対応付けます。
/// </summary>
/// <typeparam name="T">対応付ける数値型。</typeparam>
public class ThousandsSeparatorNumberBinder<T>
    : DefaultModelBinder
    where T : struct, IConvertible
{}
BindModel をオーバーライド

多少のお作法を守りながら変換するだけ。唐突に登場している Parser は後述。

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

    if (valueProviderResult == null)
    {
        return base.BindModel(controllerContext, bindingContext);
    }

    // モデルステートの設定
    bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

    // 値の文字列
    var valueString = valueProviderResult.AttemptedValue;

    // 以下、変換処理

    if (string.IsNullOrWhiteSpace(valueString))
    {
        return null;
    }

    try
    {
        return Parser(valueString);
    }
    catch (Exception ex)
    {
        bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
        return null;
    }
}
カンマ区切りの数値文字列を数値型に変換

int.ParseNumberStyles フラグを渡すことで簡単に実現できます。ただ、数値型は複数あるのでそこは頑張ります。パース時のオーバーフロー例外を期待するとか考慮するとそれぞれきっちり。

/// <summary>
/// 送信された文字列の変換に使うパーサー。
/// </summary>
private static readonly Func<string, object> Parser;

/// <summary>
/// パーサーを初期化します。
/// </summary>
static ThousandsSeparatorNumberBinder()
{
    // short, long は int と同様、float, double は decimal と同様なので割愛
    var parsers = new Dictionary<Type, Func<string, object>>
    {
        {
            typeof(int), 
            (string value) => int.Parse(value, NumberStyles.Integer | NumberStyles.AllowThousands)
        },
        {
            typeof(decimal), 
            (string value) => decimal.Parse(value, NumberStyles.Number)
        }
    };

    Parser = parsers[typeof(T)];
}

カスタムモデルバインダーの登録

アプリケーション全体、または、コントローラーのアクションで登録します。今回はプリミティブな型向けなのでアプリケーション全体で登録するのが良いかと思います。

// モデルバインダーの登録
// グローバルで一括登録するならキー型は nullable も忘れずに!
// カスタムバインダー側は nullable でなくておk。
// コントローラーやアクションでの個別利用なら適宜どうぞ。
ModelBinders.Binders.Add(typeof(int), new ThousandsSeparatorNumberBinder<int>());
ModelBinders.Binders.Add(typeof(int?), new ThousandsSeparatorNumberBinder<int>());

結果

やったね!
f:id:KatsuYuzu:20151217023340p:plain

バトンタッチ

qiita.com

*1:和暦の話は面倒なのでいつか。

*2:いつも思うんですが数値型制約とか欲しいよ!

angularjsで画面表示時に実行前の式が表示されないようにするng-cloak

angularjsで画面表示時に実行前の式が表示されないようにするng-cloak。何も対策せずに実行すると画面表示時に一瞬だけ{{hoge.piyo}}と見えたりします。それの対策。

環境

  • TypeScript 1.4
  • AngularJS 1.4.1

ng-cloak

HTMLで隠したい領域にng-cloakという属性を付けて、CSSの属性セレクターで非表示にするだけです。

<div ng-cloak>
    {{hoge.piyo}}
</div>
[ng-cloak] {
  display: none !important;
}

ng-cloak属性はangularjsのコンパイル時に削除されます。つまり、CSSで非表示にしていた属性が削除され、表示されるようになるということです。

document.titleをangularjsで制御する

似たような事象でdocument.titleで{{hoge.title}}とすると一瞬だけブラウザのタイトルに式が見えてしまいます。これはng-bindを使うことで解決します。

<!--
    angularjs の初期化前に expression が表示されないように初期値と ng-bind の両対応
-->
<title ng-bind="hoge.title">CLR/H ~Cafe~</title>

ソース

github.com

勉強会で紹介しました

clrh.connpass.com