パート 5: ビジネス ロジック

作成者: Joe Stagner

Tailspin Spyworks は、.NET プラットフォーム用の高性能でスケーラブルなアプリケーションを簡単に作成できる方法を実証します。 ASP.NET 4 の優れた新機能を使用して、ショッピング、チェックアウト、管理など、オンライン ストアを構築する方法を示します。

このチュートリアル シリーズでは、Tailspin Spyworks サンプル アプリケーションを作成するために必要なすべての手順について詳しく説明します。 パート 5 では、ビジネス ロジックが追加されます。

ビジネス ロジックの追加

Microsoft では、ユーザーが Web サイトにアクセスするたびにショッピングを体験できるようにしたいと考えています。 閲覧者は、登録またはログインしていない場合でも、アイテムを参照してショッピング カートに追加できます。 チェックアウトする準備ができたら、認証オプションが付与され、まだメンバーでない場合はアカウントを作成できます。

つまり、ショッピング カートを匿名状態から "登録ユーザー" 状態に変換するロジックを実装する必要があります。

"Classes" という名前のディレクトリを作成し、フォルダーを右クリックして、MyShoppingCart.cs という名前の新しい "Class" ファイルを作成してみましょう。

Screenshot that shows the new Class file named My Shopping Cart dot C S.

Screenshot that shows the contents of the Classes folder.

前述のとおり、MyShoppingCart.aspx ページを実装するクラスを拡張し、.NET の強力な "部分クラス" コンストラクトを使用してこれを実行します。

MyShoppingCart.aspx.cf ファイル用に生成された呼び出しは次のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace TailspinSpyworks
{
    public partial class MyShoppingCart : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }
    }
}

"Partial" キーワードの使用に注意してください。

先ほど生成したクラス ファイルは次のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace TailspinSpyworks.Classes
{
    public class MyShoppingCart
    {
    }
}

このファイルにも Partial キーワードを追加して、実装をマージします。

新しいクラス ファイルは次のようになります。

namespace TailspinSpyworks.Classes
{
    public partial class MyShoppingCart
    {
    }
}

クラスに追加する最初のメソッドは、"AddItem" メソッドです。 これは、ユーザーが [製品リスト] ページと [製品の詳細] ページの [アートに追加] リンクをクリックしたときに最終的に呼び出されるメソッドです。

ページの上部にある using ステートメントに次のコードを追加します。

using TailspinSpyworks.Data_Access;

また、このメソッドを MyShoppingCart クラスに追加します。

//------------------------------------------------------------------------------------+
public void AddItem(string cartID, int productID, int quantity)
{
  using (CommerceEntities db = new CommerceEntities())
    {
    try 
      {
      var myItem = (from c in db.ShoppingCarts where c.CartID == cartID && 
                              c.ProductID == productID select c).FirstOrDefault();
      if(myItem == null)
        {
        ShoppingCart cartadd = new ShoppingCart();
        cartadd.CartID = cartID;
        cartadd.Quantity = quantity;
        cartadd.ProductID = productID;
        cartadd.DateCreated = DateTime.Now;
        db.ShoppingCarts.AddObject(cartadd);
        }
      else
        {
        myItem.Quantity += quantity;
        }
      db.SaveChanges();
      }
    catch (Exception exp)
      {
      throw new Exception("ERROR: Unable to Add Item to Cart - " + 
                                                          exp.Message.ToString(), exp);
      }
   }
}

LINQ to Entities を使用して、商品が既にカートに入っているかどうかを確認しています。 その場合は、商品の注文数量を更新します。それ以外の場合は、選択した商品の新しいエントリを作成します。

このメソッドを呼び出すために、このメソッドをクラス化するだけでなく、商品が追加された後に現在のショッピング a=cart を表示するAddToCart.aspx ページを実装します。

ソリューション エクスプローラーでソリューション名を右クリックし、前回と同じように AddToCart.aspx という名前の新しいページを追加します。

このページを使用して、在庫が少ないなどの問題の中間結果を表示できますが、実装では、ページは実際にはレンダリングされず、"追加" ロジックを呼び出してリダイレクトします。

これを実現するには、次のコードを Page_Load イベントに追加します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Diagnostics;

namespace TailspinSpyworks
{
    public partial class AddToCart : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            string rawId = Request.QueryString["ProductID"];
            int productId;
            if (!String.IsNullOrEmpty(rawId) && Int32.TryParse(rawId, out productId))
            {
                MyShoppingCart usersShoppingCart = new MyShoppingCart();
                String cartId = usersShoppingCart.GetShoppingCartId();
                usersShoppingCart.AddItem(cartId, productId, 1);
            }
            else
            {
                Debug.Fail("ERROR : We should never get to AddToCart.aspx 
                                                   without a ProductId.");
                throw new Exception("ERROR : It is illegal to load AddToCart.aspx 
                                                   without setting a ProductId.");
            }
            Response.Redirect("MyShoppingCart.aspx");
        }
    }
}

QueryString パラメーターからショッピング カートに追加する製品を取得し、クラスの AddItem メソッドを呼び出すことに注意してください。

エラーが発生しなかったと仮定すると、コントロールは、次に完全に実装する SHoppingCart.aspx ページに渡されます。 エラーが発生した場合は、例外をスローします。

現時点では、グローバル エラー ハンドラーをまだ実装していないため、この例外はアプリケーションによって処理されませんが、まもなくこれを改善する予定です。

ステートメント Debug.Fail() の使用にも注意してください (using System.Diagnostics;) を介して使用できます

アプリケーションがデバッガー内で実行されている場合、このメソッドは、指定したエラー メッセージと共に、アプリケーションの状態に関する情報を含む詳細なダイアログを表示します。

運用環境で実行する場合、Debug.Fail() ステートメントは無視されます。

上記のコードでは、ショッピング カート クラス名 "GetShoppingCartId" のメソッドへの呼び出しに注意してください。

メソッドを実装するコードを次のように追加します。

また、更新ボタン、チェックアウト ボタン、カート "合計" を表示できるラベルも追加されたことに注意してください。

public const string CartId = "TailSpinSpyWorks_CartID";

//--------------------------------------------------------------------------------------+
public String GetShoppingCartId()
{
  if (Session[CartId] == null)
     {
     Session[CartId] = System.Web.HttpContext.Current.Request.IsAuthenticated ? 
                                        User.Identity.Name : Guid.NewGuid().ToString();
     }
  return Session[CartId].ToString();
}

ショッピング カートに商品を追加できるようになりましたが、製品が追加された後にカートを表示するロジックは実装されていません。

そのため、MyShoppingCart.aspx ページでは、EntityDataSource コントロールと GridVire コントロールを次のように追加します。

<div id="ShoppingCartTitle" runat="server" class="ContentHead">Shopping Cart</div>
<asp:GridView ID="MyList" runat="server" AutoGenerateColumns="False" ShowFooter="True" 
                          GridLines="Vertical" CellPadding="4"
                          DataSourceID="EDS_Cart"  
                          DataKeyNames="ProductID,UnitCost,Quantity" 
                          CssClass="CartListItem">              
  <AlternatingRowStyle CssClass="CartListItemAlt" />
  <Columns>
    <asp:BoundField DataField="ProductID" HeaderText="Product ID" ReadOnly="True" 
                                          SortExpression="ProductID"  />
    <asp:BoundField DataField="ModelNumber" HeaderText="Model Number" 
                                            SortExpression="ModelNumber" />
    <asp:BoundField DataField="ModelName" HeaderText="Model Name" 
                                          SortExpression="ModelName"  />
    <asp:BoundField DataField="UnitCost" HeaderText="Unit Cost" ReadOnly="True" 
                                         SortExpression="UnitCost" 
                                         DataFormatString="{0:c}" />         
    <asp:TemplateField> 
      <HeaderTemplate>Quantity</HeaderTemplate>
      <ItemTemplate>
         <asp:TextBox ID="PurchaseQuantity" Width="40" runat="server" 
                      Text='<%# Bind("Quantity") %>'></asp:TextBox> 
      </ItemTemplate>
    </asp:TemplateField>           
    <asp:TemplateField> 
      <HeaderTemplate>Item Total</HeaderTemplate>
      <ItemTemplate>
        <%# (Convert.ToDouble(Eval("Quantity")) *  
             Convert.ToDouble(Eval("UnitCost")))%>
      </ItemTemplate>
    </asp:TemplateField>
    <asp:TemplateField> 
    <HeaderTemplate>Remove Item</HeaderTemplate>
      <ItemTemplate>
        <center>
          <asp:CheckBox id="Remove" runat="server" />
        </center>
      </ItemTemplate>
    </asp:TemplateField>
  </Columns>
  <FooterStyle CssClass="CartListFooter"/>
  <HeaderStyle  CssClass="CartListHead" />
</asp:GridView>

<div>
  <strong>
    <asp:Label ID="LabelTotalText" runat="server" Text="Order Total : ">  
    </asp:Label>
    <asp:Label CssClass="NormalBold" id="lblTotal" runat="server" 
                                                   EnableViewState="false">
    </asp:Label>
  </strong> 
</div>
<br />
<asp:imagebutton id="UpdateBtn" runat="server" ImageURL="Styles/Images/update_cart.gif" 
                                onclick="UpdateBtn_Click"></asp:imagebutton>
<asp:imagebutton id="CheckoutBtn" runat="server"  
                                  ImageURL="Styles/Images/final_checkout.gif"    
                                  PostBackUrl="~/CheckOut.aspx">
</asp:imagebutton>
<asp:EntityDataSource ID="EDS_Cart" runat="server" 
                      ConnectionString="name=CommerceEntities" 
                      DefaultContainerName="CommerceEntities" EnableFlattening="False" 
                      EnableUpdate="True" EntitySetName="ViewCarts" 
                      AutoGenerateWhereClause="True" EntityTypeFilter="" Select=""                         
                      Where="">
  <WhereParameters>
    <asp:SessionParameter Name="CartID" DefaultValue="0" 
                                        SessionField="TailSpinSpyWorks_CartID" />
  </WhereParameters>
</asp:EntityDataSource>

デザイナーでフォームを呼び出して、[カートの更新] ボタンをダブルクリックし、マークアップの宣言で指定されているクリック イベント ハンドラーを生成できるようにします。

詳細は後で実装しますが、これを行うと、エラーなしでアプリケーションをビルドして実行できるようになります。

アプリケーションを実行し、ショッピング カートに商品を追加すると、このように表示されます。

Screenshot that shows the updated shopping cart.

3 つのカスタム列を実装することで、"既定" のグリッド表示から逸脱することに注意してください。

1 つ目は、Quantity の編集可能な "バインド" フィールドです。

<asp:TemplateField> 
      <HeaderTemplate>Quantity</HeaderTemplate>
      <ItemTemplate>
         <asp:TextBox ID="PurchaseQuantity" Width="40" runat="server" 
                      Text='<%# Bind("Quantity") %>'></asp:TextBox> 
      </ItemTemplate>
    </asp:TemplateField>

次の列は、商品の合計 (商品のコストに注文数を乗じたもの) を表示する "計算済み" 列です。

<asp:TemplateField> 
      <HeaderTemplate>Item Total</HeaderTemplate>
      <ItemTemplate>
        <%# (Convert.ToDouble(Eval("Quantity")) *  
             Convert.ToDouble(Eval("UnitCost")))%>
      </ItemTemplate>
    </asp:TemplateField>

最後に、ユーザーがショッピング カートから商品を削除する必要があることを示すために使用する CheckBox コントロールを含むカスタム列があります。

<asp:TemplateField> 
    <HeaderTemplate>Remove Item</HeaderTemplate>
      <ItemTemplate>
        <center>
          <asp:CheckBox id="Remove" runat="server" />
        </center>
      </ItemTemplate>
    </asp:TemplateField>

Screenshot that shows the updated Quantity and Remove Items.

ご覧のとおり、[注文合計] 行は空なので、注文合計を計算するためのロジックをいくつか追加しましょう。

まず、MyShoppingCart クラスに "GetTotal" メソッドを実装します。

MyShoppingCart.cs ファイルに次のコードを追加します。

//--------------------------------------------------------------------------------------+
public decimal GetTotal(string cartID)
{
    using (CommerceEntities db = new CommerceEntities())
    {
        decimal cartTotal = 0;
        try
        {
            var myCart = (from c in db.ViewCarts where c.CartID == cartID select c);
            if (myCart.Count() > 0)
            {
                cartTotal = myCart.Sum(od => (decimal)od.Quantity * (decimal)od.UnitCost);
            }
        }
        catch (Exception exp)
        {
            throw new Exception("ERROR: Unable to Calculate Order Total - " + 
            exp.Message.ToString(), exp);
        }
        return (cartTotal);
     }
}

次に、Page_Load イベント ハンドラーで、GetTotal メソッドを呼び出すことができます。 同時に、ショッピング カートが空かどうかを確認するテストを追加し、表示が空の場合はそれに応じて表示を調整します。

ショッピング カートが空の場合、次のようになります。

Screenshot that shows the empty shopping cart.

そうでない場合は、合計が表示されます。

Screenshot that shows the total amount for the items in the shopping cart.

ただし、このページはまだ完全ではありません。

削除対象としてマークされた商品を削除し、ユーザーによってグリッドで変更された可能性がある新しい数量の値を決定することで、ショッピング カートを再計算するためのロジックを追加する必要があります。

MyShoppingCart.cs のショッピング カート クラスに "RemoveItem" メソッドを追加して、ユーザーが商品を削除対象としてマークした場合に処理できるようにします。

//------------------------------------------------------------------------------------+
public void RemoveItem(string cartID, int  productID)
{
    using (CommerceEntities db = new CommerceEntities())
    {
        try
        {
            var myItem = (from c in db.ShoppingCarts where c.CartID == cartID && 
                         c.ProductID == productID select c).FirstOrDefault();
            if (myItem != null)
            {
                db.DeleteObject(myItem);
                db.SaveChanges();
            }
        }
        catch (Exception exp)
        {
            throw new Exception("ERROR: Unable to Remove Cart Item - " + 
                                  exp.Message.ToString(), exp);
        }
    }
}

ここで、ユーザーが GridView で注文する品質を変更するだけの場合の状況の処理方法を追加しましょう。

//--------------------------------------------------------------------------------------+
public void UpdateItem(string cartID, int productID, int quantity)
{
    using (CommerceEntities db = new CommerceEntities())
    {
        try
        {
            var myItem = (from c in db.ShoppingCarts where c.CartID == cartID && 
                    c.ProductID == productID select c).FirstOrDefault();
            if (myItem != null)
            {
                myItem.Quantity = quantity;
                db.SaveChanges();
            }
        }
        catch (Exception exp)
        {
            throw new Exception("ERROR: Unable to Update Cart Item - " +     
                                exp.Message.ToString(), exp);
        }
    }
}

基本的な削除機能と更新機能を使用することで、実際にデータベース内のショッピング カートを更新するロジックを実装できます。 (In MyShoppingCart.cs 内)

//-------------------------------------------------------------------------------------+
public void UpdateShoppingCartDatabase(String cartId, 
                                       ShoppingCartUpdates[] CartItemUpdates)
{
  using (CommerceEntities db = new CommerceEntities())
    {
    try
      {
      int CartItemCOunt = CartItemUpdates.Count();
      var myCart = (from c in db.ViewCarts where c.CartID == cartId select c);
      foreach (var cartItem in myCart)
        {
        // Iterate through all rows within shopping cart list
        for (int i = 0; i < CartItemCOunt; i++)
          {
          if (cartItem.ProductID == CartItemUpdates[i].ProductId)
             {
             if (CartItemUpdates[i].PurchaseQantity < 1 || 
   CartItemUpdates[i].RemoveItem == true)
                {
                RemoveItem(cartId, cartItem.ProductID);
                }
             else 
                {
                UpdateItem(cartId, cartItem.ProductID, 
                                   CartItemUpdates[i].PurchaseQantity);
                }
              }
            }
          }
        }
      catch (Exception exp)
        {
        throw new Exception("ERROR: Unable to Update Cart Database - " + 
                             exp.Message.ToString(), exp);
        }            
    }           
}

このメソッドには 2 つのパラメーターが必要であることに注意してください。 1 つはショッピング カート ID で、もう 1 つはユーザー定義型のオブジェクトの配列です。

ユーザー インターフェイスの仕様へのロジックの依存関係を最小限に抑えるために、メソッドが GridView コントロールに直接アクセスする必要がなくても、ショッピング カートの商品をコードに渡すために使用できるデータ構造を定義しました。

public struct ShoppingCartUpdates
{
    public int ProductId;
    public int PurchaseQantity;
    public bool RemoveItem;
}

MyShoppingCart.aspx.cs ファイルでは、次のように [更新] ボタンのクリック イベント ハンドラーでこの構造体を使用できます。 カートの更新に加えて、カートの合計も更新されることに注意してください。

//--------------------------------------------------------------------------------------+
protected void UpdateBtn_Click(object sender, ImageClickEventArgs e)
{
  MyShoppingCart usersShoppingCart = new MyShoppingCart();
  String cartId = usersShoppingCart.GetShoppingCartId();

  ShoppingCartUpdates[] cartUpdates = new ShoppingCartUpdates[MyList.Rows.Count];
  for (int i = 0; i < MyList.Rows.Count; i++)
    {
    IOrderedDictionary rowValues = new OrderedDictionary();
    rowValues = GetValues(MyList.Rows[i]);
    cartUpdates[i].ProductId =  Convert.ToInt32(rowValues["ProductID"]);
    cartUpdates[i].PurchaseQantity = Convert.ToInt32(rowValues["Quantity"]); 

    CheckBox cbRemove = new CheckBox();
    cbRemove = (CheckBox)MyList.Rows[i].FindControl("Remove");
    cartUpdates[i].RemoveItem = cbRemove.Checked;
    }

   usersShoppingCart.UpdateShoppingCartDatabase(cartId, cartUpdates);
   MyList.DataBind();
   lblTotal.Text = String.Format("{0:c}", usersShoppingCart.GetTotal(cartId));
}

次のコード行に特に関心がある場合は、注意してください。

rowValues = GetValues(MyList.Rows[i]);

GetValues() は、次のように MyShoppingCart.aspx.cs で実装する特別なヘルパー関数です。

//--------------------------------------------------------------------------------------+
public static IOrderedDictionary GetValues(GridViewRow row)
{
  IOrderedDictionary values = new OrderedDictionary();
  foreach (DataControlFieldCell cell in row.Cells)
    {
    if (cell.Visible)
      {
      // Extract values from the cell
      cell.ContainingField.ExtractValuesFromCell(values, cell, row.RowState, true);
      }
    }
    return values;
}

これにより、GridView コントロール内のバインドされた要素の値にアクセスするためのクリーンな方法が提供されます。 "Remove Item" CheckBox コントロールはバインドされていないため、FindControl() メソッドを介してアクセスします。

プロジェクト開発のこの段階では、チェックアウト プロセスを実装する準備をしています。

その前に、Visual Studio を使用してメンバーシップ データベースを生成し、メンバーシップ リポジトリにユーザーを追加しましょう。