だるろぐ

明日できることは、今日しない。

WebMatrix でユーザー認証機能(3) ―― なにはともあれユーザー登録しないと始まらん

f:id:daruyanagi:20120824233928p:plain

のんびりやっていこう。今回はユーザー登録するで。

@{
    var name = "";
    var password = "";
    var confirmPassword = "";

    if (IsPost)
    {
        name = Request.Form["name"];
        password = Request.Form["password"];
        confirmPassword = Request.Form["confirmPassword"];

        // ここでバリデーション(値が妥当なものか検証)する

        if (Validation.IsValid())
        {
            if (WebSecurity.GetUserId(name) > -1)
            {
                ModelState.AddFormError("Username alredy exists");
            }
            else
            {
                try
                {
                    WebSecurity.CreateUserAndAccount(
                        name, password, new { Name = name });
                    WebSecurity.Login(name, password);
                    Response.Redirect("~/");
                }
                catch (Exception e)
                {
                    ModelState.AddFormError(e.Message);
                }
            }
        }
    }
}

まずはバリデーションを関連の記述をとっぱらったサーバー側のコード*1。やっていることは簡単で、

  1. フォームから値を受け取る
  2. 値をバリデーション
  3. ユーザーネームにダブりがないか確認
  4. ユーザープロファイルとメンバーシップアカウントを作成
  5. ログインしてトップページへリダイレクト
  6. エラーが発生したら適宜 ModelState に登録しておく

みたいな感じ。

WebSecurity.CreateUserAndAccount()

WebSecurity では、ユーザーアカウントをユーザープロファイル(開発者が管理)とメンバーシップアカウント(システムが管理)にわけて管理している(WebMatrix でユーザー認証機能(2) ―― WebSecurityってどうやって使うんだ? - だるろぐ)。両者は同じ UserId で紐付けられる仕組みだ。

WebSecurity.CreateUserAndAccount() はそのユーザープロファイルとメンバーシップアカウントの作成を同時に行うメソッド。ユーザープロファイルテーブルに挿入するデータは、第三引数で匿名オブジェクトを与えればよい。今回は名前だけを登録しておいた。

ちなみに WebSecurity.Create() だけを使った場合、さきにユーザープロファイルテーブルへ UserId とそのほかのデータを挿入しておかなければならない。“Starter Site”テンプレートではそれを SQL で行なっている。

if (Validation.IsValid())
{
    var db = Database.Open(App.Database);

    var user = db.QuerySingle(
        "SELECT Name FROM Users WHERE LOWER(Name) = LOWER(@0)",
        name);

    if (user == null)
    {
        db.Execute(
            "INSERT INTO Users (Name) VALUES (@0)", name);
        WebSecurity.CreateAccount(name, password);

げろげろうげー。

if (Validation.IsValid())
{
    if (WebSecurity.GetUserId(name) > -1)
    {
        ModelState.AddFormError("Username alredy exists");
    }
    else
    {
        try
        {
            WebSecurity.CreateUserAndAccount(
                name, password, new { Name = name });

絶対こっちにするやろ。せっかく WebSecurity Helper を使うんだから、大いに頼ればよろしい。

f:id:daruyanagi:20120825001002p:plain

ModelState

ModelState は WebPage に属しており、フォームのエラーを管理するものだとでも理解しておく。 ModelState.AddFormError() でフォーム関連のエラーが登録できるんだな。

バリデーション関連のコードはこんな感じ(“Starter Site”からコードをパクってきた)。

Validation.RequireField("name", 
    "電子メール アドレスを入力してください。");
Validation.RequireField("password", 
    "パスワードを空白にすることはできません。");
Validation.Add("confirmPassword", Validator.EqualsTo(
    "password", "パスワードと確認のパスワードが一致しません。")
);
Validation.Add("password",
    Validator.StringLength(
        maxLength: Int32.MaxValue,
        minLength: 6,
        errorMessage: "パスワードは 6 文字以上にする必要があります"
    )
);

読むだけで何をしているのかわかるのがいい。表示部分はこのようなコードになっていた。

<section id="register">
    <form method="post">
        @Html.ValidationSummary("Log in was unsuccessful." + 
            "Please correct the errors and try again.",
            excludeFieldErrors: true, htmlAttributes: null)

        <fieldset>
            <legend>Register Your Account</legend>
            <ol>
                @RenderFormInputWithValidation(this, "name")
                <li class="name">
                    <label for="name" @if (!ModelState.IsValidField("name")) {<text>class="error-label"</text>}>Name</label> 
                    <input type="text"
                        id="name" name="name" value="@name"
                        @Validation.For("name") />
                    @Html.ValidationMessage("name")
                </li>
                <li class="password">
                    <label for="password" @if (!ModelState.IsValidField("password")) {<text>class="error-label"</text>}>Password</label> 
                    <input type="password"
                        id="password" name="password"
                        @Validation.For("password")/>
                    @Html.ValidationMessage("password")
                </li>
                <li class="confirm-password">
                    <label for="confirm-password" @if (!ModelState.IsValidField("confirmPassword")) {<text>class="error-label"</text>}>confirmPassword</label> 
                    <input type="password"
                        id="confirmPassword" name="confirmPassword"
                        @Validation.For("confirmPassword")/>
                    
                    @Html.ValidationMessage("confirmPassword")
                </li>
            </ol>
            <input type="submit" value="Register" />
        </fieldset>
    </form>
</section>

ぐちゃぐちゃしてわかりにくいが、

@Html.ValidationSummary("Log in was unsuccessful." + 
    "Please correct the errors and try again.",
    excludeFieldErrors: true, htmlAttributes: null)

でページ全体についてのバリデーション結果を表示する。

f:id:daruyanagi:20120825002435p:plain

フォームの各フィールドは、

<label for="name" @if (!ModelState.IsValidField("name")) {<text>class="error-label"</text>}>Name</label> 
<input type="text" id="name" name="name" value="@name" @Validation.For("name") />
@Html.ValidationMessage("name")

このコードでワンセットみたい。 Validation.For() は JavaScript によるバリデーションに必要な data- 属性を出力する(今回は使ってない)。 Html.ValidationMessage() はバリデーションエラーのメッセージがあれば表示する。このメッセージはさっき ModelState に登録したエラーから取得するみたい。

f:id:daruyanagi:20120825002611p:plain

後者のコードは使いまわせそうないわばイディオムなので、ヘルパーか拡張メソッドにしておくとよさそうだ。今回は以下のようにしてみたよ。

~/App_Code/WebPageExtension.cs

using System.Linq;
using System.Web;
using System.Web.WebPages;
using System.Web.WebPages.Html;

public static class WebPageExtension
{
    private enum InputType
    {
        Text, Password, Checkbox, Textarea,
    }

    private static HtmlString RenderInputWithValidation(
        this WebPage target, InputType input_type,
        string name, string label = "", object additional = null)
    {
        const string HTML_TAG = @"
            <div class=""validation-input {0} {1}"">
                <label for=""{0}"">{2}</label>
                <input type=""{3}"" id=""{0}"" name=""{0}"" {4} {5} />
                {6}
            </div>";
            
        var attrs = (additional == null)
            ? string.Empty
            : string.Join(" ", additional
                .GetType()
                .GetProperties()
                .Select((p) => string
                    .Format("{0}=\"{1}\"",
                        p.Name.ToLower(),
                        p.GetValue(additional)
                    )
                )
            );

        return new HtmlString(
            string.Format(
                HTML_TAG, name,
                target.ModelState.IsValidField(name)
                    ? string.Empty
                    : "validation-failed",
                label.IsEmpty() ? name : label,
                input_type.ToString().ToLower(),
                attrs,
                target.Validation.For(name),
                target.Html.ValidationMessage(name)
            )
        );
    }

    public static HtmlString RenderTextWithValidation(
        this WebPage target, string name,
        string label = null, object additional = null)
    {
        return RenderInputWithValidation(
            target, InputType.Text, name, label, additional);
    }

    public static HtmlString RenderPasswordWithValidation(
        this WebPage target, string name,
        string label = null, object additional = null)
    {
        return RenderInputWithValidation(
            target, InputType.Password, name, label, additional);
    }

    public static HtmlString RenderCheckBoxWithValidation(
        this WebPage target, string name,
        string label = null, object additional = null)
    {
        return RenderInputWithValidation(
            target, InputType.Checkbox, name, label, additional);
    }
}

ModelState が WebPage に属する関係で、 WebPage クラスの拡張メソッドとして定義してある。ほんとは HTML Helper にして、 @Html.TextWithValidation() などと呼べる方がカッコいいな。今度もう少しこのあたりはブラッシュアップしてみたい。

んで、これを使って書きなおした表示部分はこうなる。

<section id="register">
    <form method="post">
        @Html.ValidationSummary(
            "Log in was unsuccessful." + 
            "Please correct the errors and try again.",
            false, null)

        <fieldset>
            <legend>Register Your Account</legend>
            @this.RenderTextWithValidation(
                "name", "Name", new { Value = name} )
            @this.RenderPasswordWithValidation(
                "password", "Password")
            @this.RenderPasswordWithValidation(
                "confirmPassword", "Confirm Password")
            <input type="submit" value="Register" />
        </fieldset>
    </form>
</section>

だいぶシンプルになって予はだいぶ満足したぞよ。ほかのヘルパーの真似して new { Value = name} で追加の属性を追加できるようにしてみたのがちょっとだけ自慢だ。リフレクションはちょっと苦手なので、今日は個人的にその勉強などしてみた。

次回はログインとログアウトだけど、これは一瞬で終わりそう……。

*1:コードビハインドっていうの? 知らんけど