give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

O/Rマッピングツールに対する誤解をときたい -実装編 Part3-

Part2を読む

実装コード

それではここからいよいよ実装コードの説明を進めていきます。
書籍注文ページを実装するには以下のようなクラスやファイルが必要になってきます。

  • 書籍注文ページのUI(aspx + aspx.cs)
  • ドメインモデルを構成する注文クラス、注文明細クラス、書籍クラス、和書クラス、洋書クラス
  • 注文処理を実行する注文Facadeクラス
  • 書籍のドロップダウンリストに表示する情報を返す書籍Facadeクラス
  • NHibernateの利用を簡略化するヘルパークラス


極限まで仕様を少なくしたつもりなのですが、それでもブログ上でプログラムを説明しようとすると結構なボリュームになってしまいます。
最初はちょっと面食らってしまうかもしれませんが、要点をおさえていけばそれほど難しくない・・・かな???

UIの実装

書籍注文ページのUI(BookOrderForm.aspx)
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="BookOrderForm.aspx.cs" Inherits="BookOrderForm" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>書籍注文ページ</title>
</head>
<body>
    <form id="form1" runat="server">
    <h3>書籍注文ページ</h3>
    <div>
        <!-- 書籍のドロップダウンリストはObjectDataSourceとBookFacadeで作成する。ただしテキスト表示はコードビハインド側でカスタマイズする。 -->
        <asp:ObjectDataSource ID="BookDataSource" runat="server" SelectMethod="FindAllBooks"
            TypeName="Junichi.Ito.NHibernateBookStore.Facade.BookFacade" OnSelected="BookDataSource_Selected"></asp:ObjectDataSource>
        注文者<asp:TextBox ID="CustomerTextBox" runat="server"></asp:TextBox><br />
        <br />
        注文リスト<br />
        <asp:GridView ID="BookOrderGridView" runat="server" AutoGenerateColumns="False" ShowFooter="True" OnDataBound="BookOrderGridView_DataBound" OnSelectedIndexChanging="BookOrderGridView_SelectedIndexChanging" OnRowDataBound="BookOrderGridView_RowDataBound">
            <Columns>
                <asp:TemplateField ShowHeader="False">
                    <FooterTemplate>
                        <asp:LinkButton ID="InsertLinkButton" runat="server" CausesValidation="False" OnClick="InsertLinkButton_Click"
                            Text="追加"></asp:LinkButton>
                    </FooterTemplate>
                    <ItemTemplate>
                        <asp:LinkButton ID="DeleteLinkButton" runat="server" CausesValidation="False" CommandName="Select"
                            Text="削除"></asp:LinkButton>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField HeaderText="注文する書籍">
                    <EditItemTemplate>
                        <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
                    </EditItemTemplate>
                    <ItemTemplate>
                        <asp:DropDownList ID="BookDropDownList" runat="server" DataSourceID="BookDataSource" DataValueField="ID" OnDataBound="BookDropDownList_DataBound">
                        </asp:DropDownList>
                    </ItemTemplate>
                    <FooterTemplate>
                        <asp:DropDownList ID="BookDropDownList" runat="server" DataSourceID="BookDataSource" DataValueField="ID" OnDataBound="BookDropDownList_DataBound">
                        </asp:DropDownList>
                    </FooterTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>
        <br />
        <asp:Button ID="OrderButton" runat="server" Text="注文する" OnClick="OrderButton_Click" /><br />
        <br />
        <asp:HyperLink ID="BackMenuHyperLink" runat="server" NavigateUrl="~/Default.aspx">メニューに戻る</asp:HyperLink></div>
    </form>
</body>
</html>


この画面では書籍のドロップダウンリストが表示されますが、このデータの取得はObjectDataSourceを経由して行います。

<asp:ObjectDataSource ID="BookDataSource" runat="server" SelectMethod="FindAllBooks"
            TypeName="Junichi.Ito.NHibernateBookStore.Facade.BookFacade" OnSelected="BookDataSource_Selected"></asp:ObjectDataSource>


ObjectDataSourceは書籍Facadeの「すべての書籍を返す」メソッドを使います。
このメソッドのメソッドシグニチャは以下の通りです。

public IList<Book> FindAllBooks()


書籍Facadeについては詳しくは後ほど説明します。
また、ドロップダウンリストの表示は「(書籍名) [和書/洋書 在庫あり/なし]」のような表示になっていますが、この表示の加工は次に説明するコードビハインドクラス内のBookDropDownList_DataBoundメソッド(イベント)で行っています。

書籍注文ページのコードビハインドクラス(BookOrderForm.aspx.cs)
using System;
using System.Collections.Generic;
using System.Web.UI.WebControls;

using Junichi.Ito.NHibernateBookStore;
using Junichi.Ito.NHibernateBookStore.Facade;

// 書籍注文ページ
public partial class BookOrderForm : System.Web.UI.Page
{
    // 注文Facade
    private readonly BookOrderFacade bookOrderFacade = new BookOrderFacade();

    // ドロップダウンリストの書籍名表示をカスタマイズするために書籍情報を
    // 一時的に保存しておくルックアップテーブル
    private readonly IDictionary<int, Book> bookTable = new Dictionary<int, Book>();

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!this.IsPostBack)
        {
            // 初回のみグリッドにダミー行を挿入
            // (全行がなくなると新規書籍追加用のフッター行が表示されなくなるため)
            int[] dummyRow = new int[1];
            this.BookOrderGridView.DataSource = dummyRow;
            this.BookOrderGridView.DataBind();
        }
    }

    // "注文する"ボタンがクリックした際に呼ばれる
    protected void OrderButton_Click(object sender, EventArgs e)
    {
        // 画面の入力値を受け取る
        string customerName = this.CustomerTextBox.Text;
        IList<int> bookIDs = this.GetSelectedBookIDs();

        // 注文を実行する
        BookOrder bookOrder = this.bookOrderFacade.CreateBookOrder(customerName, bookIDs);

        // 次の画面へ注文情報を引き渡す
        this.Session["BookOrder"] = bookOrder;
        this.Response.Redirect("BookOrderCompleted.aspx");
    }

    // ドロップダウンリスト用の書籍情報が取得された後に呼び出される
    protected void BookDataSource_Selected(object sender, ObjectDataSourceStatusEventArgs e)
    {
        // すでにテーブルが作られていれば処理をスキップする
        if (this.bookTable.Count > 0)
        {
            return;
        }

        // 書籍情報をルックアップテーブルに保存する
        IList<Book> books = e.ReturnValue as IList<Book>;
        foreach (Book book in books)
        {
            this.bookTable.Add(book.ID, book);
        }
    }

    // 書籍情報のドロップダウンリストが作られた後に呼ばれる
    protected void BookDropDownList_DataBound(object sender, EventArgs e)
    {
        // 書籍名の表示をカスタマイズする
        DropDownList dropDownList = sender as DropDownList;
        foreach (ListItem listItem in dropDownList.Items)
        {
            // ルックアップテーブルから書籍情報を取得する
            int bookID = int.Parse(listItem.Value);
            Book book = this.bookTable[bookID];

            // 書籍名表示をカスタマイズする
            string bookType = book.Is(typeof(JapaneseBook)) ? "和書" : "洋書";
            string hasStock = book.HasStock ? "あり" : "なし";
            string text = string.Format("{0} [{1} 在庫{2}]", book.BookName, bookType, hasStock);
            listItem.Text = text;
        }
    }

    // グリッドの追加ボタンをクリックした際に呼ばれる
    protected void InsertLinkButton_Click(object sender, EventArgs e)
    {
        // 選択された書籍のIDを取得する
        GridViewRow row = this.BookOrderGridView.FooterRow;
        DropDownList book = row.FindControl("BookDropDownList") as DropDownList;
        int newBookID = int.Parse(book.SelectedValue);

        // グリッドに新しい書籍を追加する
        IList<int> selectedBookIDs = this.GetSelectedBookIDs();
        selectedBookIDs.Add(newBookID);

        // グリッドの表示を更新する
        this.RefleshGridDataSource(selectedBookIDs);
    }

    // グリッドの削除ボタンをクリックした際に呼ばれる
    protected void BookOrderGridView_SelectedIndexChanging(object sender, GridViewSelectEventArgs e)
    {
        // 選択された行番号を取得する
        int selectedIndex = e.NewSelectedIndex;

        // 選択された行を削除する
        IList<int> selectedBookIDs = this.GetSelectedBookIDs();
        selectedBookIDs.RemoveAt(selectedIndex);

        // グリッドの表示を更新する
        this.RefleshGridDataSource(selectedBookIDs);
    }

    // グリッドのデータバインドが完了した後に呼ばれる
    protected void BookOrderGridView_DataBound(object sender, EventArgs e)
    {
        // 1件しかない場合は削除を禁止する
        // (全行がなくなると新規書籍追加用のフッター行が表示されなくなるため)
        if (this.BookOrderGridView.Rows.Count == 1)
        {
            GridViewRow row = this.BookOrderGridView.Rows[0];
            LinkButton deleteLinkButton = row.FindControl("DeleteLinkButton") as LinkButton;
            deleteLinkButton.Visible = false;
        }
    }

    // グリッドの各行のデータバインドが完了した後に呼び出される
    protected void BookOrderGridView_RowDataBound(object senderGridView, GridViewRowEventArgs e)
    {
        // 初回アクセス時はダミー行しかないので選択値を設定しない
        if (!this.IsPostBack)
        {
            return;
        }

        // ドロップダウンリストの選択値を設定する
        GridViewRow row = e.Row;
        if (row.RowType == DataControlRowType.DataRow)
        {
            int bookID = (int)row.DataItem;
            DropDownList bookDropDownList = row.FindControl("BookDropDownList") as DropDownList;
            bookDropDownList.SelectedValue = bookID.ToString();
        }
    }

    // グリッドの表示を更新する
    private void RefleshGridDataSource(IList<int> selectedBookIDs)
    {
        this.BookOrderGridView.DataSource = selectedBookIDs;
        this.BookOrderGridView.DataBind();
    }

    // グリッドで選択されている書籍IDのリストを返す
    private IList<int> GetSelectedBookIDs()
    {
        IList<int> bookIDs = new List<int>();
        foreach (GridViewRow row in this.BookOrderGridView.Rows)
        {
            DropDownList bookDropDownList = row.FindControl("BookDropDownList") as DropDownList;
            int bookID = int.Parse(bookDropDownList.SelectedValue);
            bookIDs.Add(bookID);
        }

        return bookIDs;
    }
}


がちゃがちゃと色々書いていますが、大半は画面制御のために必要なコードばかりです。
このサンプルプログラムで重要なのは以下の部分です。

    // 注文Facade
    private readonly BookOrderFacade bookOrderFacade = new BookOrderFacade();

    // "注文する"ボタンがクリックした際に呼ばれる
    protected void OrderButton_Click(object sender, EventArgs e)
    {
        // 画面の入力値を受け取る
        string customerName = this.CustomerTextBox.Text;
        IList<int> bookIDs = this.GetSelectedBookIDs();

        // 注文を実行する
        BookOrder bookOrder = this.bookOrderFacade.CreateBookOrder(customerName, bookIDs);

        // 次の画面へ注文情報を引き渡す
        this.Session["BookOrder"] = bookOrder;
        this.Response.Redirect("BookOrderCompleted.aspx");
    }


「CreateBookOrder」というのが、注文Facadeの「新しい注文を登録する」メソッドにあたります。
メソッドシグニチャは以下の通りです。

public BookOrder CreateBookOrder(string customerName, IList<int> bookIDs)


ご覧の通り、Smart UIパターンではないのでビジネスロジックは注文Facade内で実装され、UIには現れてきません。


サンプルプログラムはまだまだ続くので、ドメインモデルクラス以降の説明は次回に行います。


Part4を読む