次の方法で共有


パート 9: 登録と精算

作成者: Jon Galloway

MVC Music Store は、ASP.NET MVC と Visual Studio を使用した Web 開発の手順を段階的に紹介し、説明するチュートリアル アプリケーションです。

MVC Music Store は、音楽アルバムをオンラインで販売する軽量なサンプル ストアの実装です。基本的なサイト管理、ユーザー サインイン、ショッピング カート機能を実装しています。

このチュートリアル シリーズでは、ASP.NET MVC Music Store サンプル アプリケーションを作成するために必要なすべての手順について詳しく説明します。 パート 9 では、登録と精算について説明します。

このセクションでは、買い物客の住所と支払い情報を収集する CheckoutController を作成します。 ユーザーは精算の前にサイトに登録することが求められるため、このコントローラーには承認が必要になります。

ユーザーは、[Checkout] (精算) ボタンをクリックして、ショッピング カートから精算プロセスに移動します。

Screenshot of the Music Store window showing the checkout view with the Checkout button highlighted by a red arrow.

ユーザーがログインしていない場合は、ログインを促すダイアログが表示されます。

Screenshot of the Music Store window showing the log on view with User name and Password fields.

ユーザーがログインに成功すると、[Address and Payment] (住所と支払い) ビューが表示されます。

Screenshot of the Music Store window showing the address and payment view with fields to collect shipping address and payment information.

フォームに入力して注文を送信すると、注文確認画面が表示されます。

Screenshot of the Music Store window showing the checkout complete view that informs the user that the order is complete.

存在しない注文または自分に属していない注文を表示しようとすると、[エラー] ビューが表示されます。

Screenshot of the Music Store window showing the error view when the user attempts to view another person's order or a fictitious order.

ショッピング カートの移行

ショッピング プロセスは匿名ですが、ユーザーが [Checkout] (精算) ボタンをクリックすると、登録とログインが必要になります。 ユーザーは、アクセスとアクセスの間にショッピング カートの情報が維持されることを期待するため、ユーザーが登録またはログインを完了したときに、ショッピング カート情報をそのユーザーに関連付ける必要があります。

これを行うことは実際には非常に簡単です。ShoppingCart クラスには、現在のカート内のすべての項目をユーザー名に関連付けるメソッドが既に用意されているためです。 ユーザーが登録またはログインを完了したときに、このメソッドを呼び出すだけで済みます。

メンバーシップと承認の設定時に追加した AccountController クラスを開きます。 MvcMusicStore.Models を参照する using ステートメントを追加し、次の MigrateShoppingCart メソッドを追加します。

private void MigrateShoppingCart(string UserName)
{
    // Associate shopping cart items with logged-in user
    var cart = ShoppingCart.GetCart(this.HttpContext);
 
    cart.MigrateCart(UserName);
    Session[ShoppingCart.CartSessionKey] = UserName;
}

次に、以下に示すように、ユーザーの検証後に MigrateShoppingCart を呼び出すように LogOn post アクションを変更します。

//
// POST: /Account/LogOn
[HttpPost]
 public ActionResult LogOn(LogOnModel model, string returnUrl)
 {
    if (ModelState.IsValid)
    {
        if (Membership.ValidateUser(model.UserName, model.Password))
        {
            MigrateShoppingCart(model.UserName);
                    
            FormsAuthentication.SetAuthCookie(model.UserName,
                model.RememberMe);
            if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1
                && returnUrl.StartsWith("/")
                && !returnUrl.StartsWith("//") &&
                !returnUrl.StartsWith("/\\"))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", "The user name or password provided is incorrect.");
        }
    }
    // If we got this far, something failed, redisplay form
    return View(model);
 }

ユーザー アカウントが正常に作成された直後に、Register post アクションに同じ変更を加えます。

//
// POST: /Account/Register
[HttpPost]
 public ActionResult Register(RegisterModel model)
 {
    if (ModelState.IsValid)
    {
        // Attempt to register the user
        MembershipCreateStatus createStatus;
        Membership.CreateUser(model.UserName, model.Password, model.Email, 
               "question", "answer", true, null, out
               createStatus);
 
        if (createStatus == MembershipCreateStatus.Success)
        {
            MigrateShoppingCart(model.UserName);
                    
            FormsAuthentication.SetAuthCookie(model.UserName, false /*
                  createPersistentCookie */);
            return RedirectToAction("Index", "Home");
        }
        else
        {
            ModelState.AddModelError("", ErrorCodeToString(createStatus));
        }
    }
    // If we got this far, something failed, redisplay form
    return View(model);
 }

これで、登録またはログインに成功すると、匿名のショッピング カートがユーザー アカウントに自動的に転送されるようになります。

CheckoutController の作成

Controllers フォルダーを右クリックし、Empty controller テンプレートを使用して、CheckoutController という名前のプロジェクトに新しいコントローラーを追加します。

Screenshot of the Add Controller window with the Controller name field filled with the text Checkout Controller.

まず、Controller クラス宣言の上に Authorize 属性を追加して、ユーザーが精算の前に登録するように要求します。

namespace MvcMusicStore.Controllers
{
    [Authorize]
    public class CheckoutController : Controller

注: これは、以前に StoreManagerController に加えた変更に似ていますが、その場合は、Authorize 属性でユーザーを管理者の役割にする必要がありました。 精算コントローラーでは、ユーザーのログインを必要としていますが、管理者であることは要求していません。

わかりやすくするために、このチュートリアルでは支払い情報を扱いません。 代わりに、ユーザーがキャンペーン コードを使って精算できるようにしています。 このキャンペーン コードは、PromoCode という定数を使用して保存します。

StoreController と同様に、MusicStoreEntities クラスのインスタンスを保持する、storeDB という名前のフィールドを宣言します。 MusicStoreEntities クラスを使用するには、MvcMusicStore.Models 名前空間の using ステートメントを追加する必要があります。 以下に、精算コントローラーの上部を示します。

using System;
using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.Controllers
{
    [Authorize]
    public class CheckoutController : Controller
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        const string PromoCode = "FREE";

CheckoutController には、次のコントローラー アクションがあります。

AddressAndPayment (GET メソッド) は、ユーザーが自分の情報を入力できるようにするフォームを表示します。

AddressAndPayment (POST メソッド) は入力を検証し、注文を処理します。

Complete は、ユーザーが精算プロセスを正常に完了した後に表示されます。 このビューには、確認としてユーザーの注文番号が含まれます。

まず、(コントローラーの作成時に生成された) インデックス コントローラー アクションの名前を AddressAndPayment に変更します。 このコントローラー アクションは、精算フォームを表示するだけなので、モデル情報は必要ありません。

//
// GET: /Checkout/AddressAndPayment
public ActionResult AddressAndPayment()
{
    return View();
}

AddressAndPayment POST メソッドは、StoreManagerController で使用したのと同じパターンに従います。これは、フォームの送信を受け入れて注文を完了しようと試み、失敗した場合はフォームを再表示します。

フォーム入力が Order の検証要件を満たしていることを検証した後、PromoCode フォームの値を直接確認します。 すべてが正しいと仮定して、更新された情報を注文と一緒に保存し、ShoppingCart オブジェクトに注文プロセスを完了するよう指示して、Complete アクションにリダイレクトします。

//
// POST: /Checkout/AddressAndPayment
[HttpPost]
public ActionResult AddressAndPayment(FormCollection values)
{
    var order = new Order();
    TryUpdateModel(order);
 
    try
    {
        if (string.Equals(values["PromoCode"], PromoCode,
            StringComparison.OrdinalIgnoreCase) == false)
        {
            return View(order);
        }
        else
        {
            order.Username = User.Identity.Name;
            order.OrderDate = DateTime.Now;
 
            //Save Order
            storeDB.Orders.Add(order);
            storeDB.SaveChanges();
            //Process the order
            var cart = ShoppingCart.GetCart(this.HttpContext);
            cart.CreateOrder(order);
 
            return RedirectToAction("Complete",
                new { id = order.OrderId });
        }
    }
    catch
    {
        //Invalid - redisplay with errors
        return View(order);
    }
}

精算プロセスが正常に完了すると、ユーザーは [Complete controller] (コントローラーの完了) アクションにリダイレクトされます。 このアクションは、確認のために注文番号を表示する前に、注文が実際にログイン ユーザーに属していることを検証するための簡単なチェックを実行します。

//
// GET: /Checkout/Complete
public ActionResult Complete(int id)
{
    // Validate customer owns this order
    bool isValid = storeDB.Orders.Any(
        o => o.OrderId == id &&
        o.Username == User.Identity.Name);
 
    if (isValid)
    {
        return View(id);
    }
    else
    {
        return View("Error");
    }
}

"注: プロジェクトを開始したときに、[エラー] ビューが /Views/Shared フォルダーに自動的に作成されました。"

CheckoutController の完全なコードは次のとおりです。

using System;
using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.Controllers
{
    [Authorize]
    public class CheckoutController : Controller
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        const string PromoCode = "FREE";
        //
        // GET: /Checkout/AddressAndPayment
        public ActionResult AddressAndPayment()
        {
            return View();
        }
        //
        // POST: /Checkout/AddressAndPayment
        [HttpPost]
        public ActionResult AddressAndPayment(FormCollection values)
        {
            var order = new Order();
            TryUpdateModel(order);
 
            try
            {
                if (string.Equals(values["PromoCode"], PromoCode,
                    StringComparison.OrdinalIgnoreCase) == false)
                {
                    return View(order);
                }
                else
                {
                    order.Username = User.Identity.Name;
                    order.OrderDate = DateTime.Now;
 
                    //Save Order
                    storeDB.Orders.Add(order);
                    storeDB.SaveChanges();
                    //Process the order
                    var cart = ShoppingCart.GetCart(this.HttpContext);
                    cart.CreateOrder(order);
 
                    return RedirectToAction("Complete",
                        new { id = order.OrderId });
                }
            }
            catch
            {
                //Invalid - redisplay with errors
                return View(order);
            }
        }
        //
        // GET: /Checkout/Complete
        public ActionResult Complete(int id)
        {
            // Validate customer owns this order
            bool isValid = storeDB.Orders.Any(
                o => o.OrderId == id &&
                o.Username == User.Identity.Name);
 
            if (isValid)
            {
                return View(id);
            }
            else
            {
                return View("Error");
            }
        }
    }
}

AddressAndPayment ビューの追加

次に、AddressAndPayment ビューを作成してみましょう。 AddressAndPayment コントローラー アクションのいずれかを右クリックし、次に示すように、Order として厳密に型指定され、Edit テンプレートを使用する AddressAndPayment という名前のビューを追加します。

Screenshot of the Add View window with the View name field, the Create a view checkbox, and the Model class and Scaffold dropdowns highlighted in red.

このビューでは、StoreManagerEdit ビューを作成するときに確認した以下の 2 つの手法を使用します。

  • Html.EditorForModel() を使用して、Order モデルのフォーム フィールドを表示します
  • 検証属性を持つ Order クラスを使用して検証規則を利用します

まず、Html.EditorForModel() を使用するようにフォーム コードを更新し、続いてキャンペーン コード用のテキスト ボックスを追加します。 AddressAndPayment ビューの完全なコードを次に示します。

@model MvcMusicStore.Models.Order
@{
    ViewBag.Title = "Address And Payment";
}
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"
type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"
type="text/javascript"></script>
@using (Html.BeginForm()) {
    
    <h2>Address And Payment</h2>
    <fieldset>
        <legend>Shipping Information</legend>
        @Html.EditorForModel()
    </fieldset>
    <fieldset>
        <legend>Payment</legend>
        <p>We're running a promotion: all music is free 
            with the promo code: "FREE"</p>
        <div class="editor-label">
            @Html.Label("Promo Code")
        </div>
        <div class="editor-field">
            @Html.TextBox("PromoCode")
        </div>
    </fieldset>
    
    <input type="submit" value="Submit Order" />
}

Order の検証規則の定義

ビューが設定されたので、以前に Album モデルに対して行ったように、Order モデルに検証規則を設定します。 Models フォルダーを右クリックし、Order という名前のクラスを追加します。 以前に Album に使用した検証属性に加えて、ユーザーのメール アドレスを検証するために正規表現も使用します。

using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace MvcMusicStore.Models
{
    [Bind(Exclude = "OrderId")]
    public partial class Order
    {
        [ScaffoldColumn(false)]
        public int OrderId { get; set; }
        [ScaffoldColumn(false)]
        public System.DateTime OrderDate { get; set; }
        [ScaffoldColumn(false)]
        public string Username { get; set; }
        [Required(ErrorMessage = "First Name is required")]
        [DisplayName("First Name")]
        [StringLength(160)]
        public string FirstName { get; set; }
        [Required(ErrorMessage = "Last Name is required")]
        [DisplayName("Last Name")]
        [StringLength(160)]
        public string LastName { get; set; }
        [Required(ErrorMessage = "Address is required")]
        [StringLength(70)]
        public string Address { get; set; }
        [Required(ErrorMessage = "City is required")]
        [StringLength(40)]
        public string City { get; set; }
        [Required(ErrorMessage = "State is required")]
        [StringLength(40)]
        public string State { get; set; }
        [Required(ErrorMessage = "Postal Code is required")]
        [DisplayName("Postal Code")]
        [StringLength(10)]
        public string PostalCode { get; set; }
        [Required(ErrorMessage = "Country is required")]
        [StringLength(40)]
        public string Country { get; set; }
        [Required(ErrorMessage = "Phone is required")]
        [StringLength(24)]
        public string Phone { get; set; }
        [Required(ErrorMessage = "Email Address is required")]
        [DisplayName("Email Address")]
       
        [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}",
            ErrorMessage = "Email is is not valid.")]
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
        [ScaffoldColumn(false)]
        public decimal Total { get; set; }
        public List<OrderDetail> OrderDetails { get; set; }
    }
}

不足または無効な情報があるフォームを送信しようとすると、クライアント側の検証を使用してエラー メッセージが表示されるようになりました。

Screenshot of the Music Store window showing the address and payment view with a string of invalid information in the phone and email fields.

これで、精算プロセスのための大変な作業のほとんどは終わりました。あとは、いくつかの簡単な作業を行うだけです。 2 つの単純なビューを追加し、ログイン プロセス中にカート情報のハンドオフを処理する必要があります。

[Checkout Complete] (精算完了) ビューの追加

[Checkout Complete] (精算完了) ビューは、注文 ID を表示するだけなので、非常にシンプルです。 [Complete controller] (コントローラーの完了) アクションを右クリックして、Complete という名前のビューを追加します。これは int として厳密に型指定されています。

Screenshot of the Add View window with the View name field and the Model class dropdown highlighted in red rectangles.

次に示すように、注文 ID を表示するようにビューのコードを更新します。

@model int
@{
    ViewBag.Title = "Checkout Complete";
}
<h2>Checkout Complete</h2>
<p>Thanks for your order! Your order number is: @Model</p>
<p>How about shopping for some more music in our 
    @Html.ActionLink("store",
"Index", "Home")
</p>

[エラー] ビューの更新

既定のテンプレートには、サイト内の別の場所で再利用できるように、[共有ビュー] フォルダーに [エラー] ビューが含まれています。 この [エラー] ビューには非常に単純なエラーが含まれており、サイト レイアウトを使用しないため、更新する予定です。

これは一般的なエラー ページであるため、コンテンツは非常に単純です。 ユーザーがアクションを再試行する場合に、メッセージと、履歴の前のページに移動するためのリンクを含める予定です。

@{
    ViewBag.Title = "Error";
}
 
<h2>Error</h2>
 
<p>We're sorry, we've hit an unexpected error.
    <a href="javascript:history.go(-1)">Click here</a> 
    if you'd like to go back and try that again.</p>