10.メイン画面-MVC編

初めに

 Web Form で中心となるのは Web フォームクラスです。そのため、画面の数分の Web フォームクラスを作成することから作業が始まります。
 一方 Web MVC で中心的な役割を果たすのはコントローラクラスです。画面の数が何枚あっても、処理単位はコントローラの数で決まります。  MVC 版のフィールドワークデータ操作画面は、すべて一つのコントローラが担当することにしました。フィールドワークデータの検索・追加・更新・削除などの操作ですから、 一つのコントローラで実装するのが最適と思います。
 それでは、状態遷移図の議論から始めましょう。

状態遷移図

 フィールドワークデータを操作する状態遷移図を示します。

画面1

 上図で示したように、6つの状態を持ちます。

  • 初期状態
    ブラウザからの初めての要求を受けた状態です
  • 追加
    初期状態から、「追加」ボタンのクリックで遷移してきます
    この状態から、「保存」ボタンのクリックで初期化状態に遷移します
  • 検索後
    初期状態から、「検索」ボタンのクリックで遷移してきます
    この状態から、「更新」ボタンのクリックで更新状態に遷移します
    この状態から、「削除」ボタンのクリックで削除状態に遷移します
    この状態から、「クリア」ボタンのクリックで初期状態に遷移します(状態遷移図には描かれていません)
  • 更新
    検索後状態から、「更新」ボタンのクリックで遷移してきます
    この状態から、「保存」ボタンのクリックで初期状態に遷移します
  • 削除
    検索後状態から、「削除」ボタンのクリックで遷移してきます
    この状態から、「削除」ボタンのクリックで初期状態に遷移します
  • 参照
    検索後状態から、「参照」ボタンのクリックで遷移してきます
    この状態から、「戻る」ボタンのクリックで初期状態に遷移します

一覧画面-特別編

 この特別編で取り上げるのは、ページングの機能に関してです。 Web Form の場合には、標準でページングの機能が提供されます。しかし MVC では、ページングは自前で用意する必要があります。 ページングを利用している画面を次に示します。フィールドワークデータを検索して、その結果が複数ページにわたる場合に必要になります。(画像をクリックすると、拡大表示します)

画面2

 この自前のページングをどのように実装するか?次のようにすることにしました。

  • ページング用モデル
    ページングに必要な機能を網羅したモデルを作成し、ページングが必要なビューのモデルクラスは、このクラスから派生して利用することとします
    ページング用モデルクラスは、特定のビューに依存することなく、ライブラリ化が可能となるように設計・実装します
  • ページング用部分ビュー
    ページングは、先頭ページに移動・前ページに移動・ページ番号で移動・次ページに移動・最終ページに移動の5項目から構成されます
    特に、ページ番号で移動するには、データの総件数や現在何ページ目を表示しているかによって動的に変化する必要があります
    この表示部分を部分ビューとして実装します
    ページングが必要なビューは、必要となる位置にこの部分ビューを組み込むことでページングが可能になるようにします

ページング用モデル

 ページングの主役となるのは、このモデルクラスです。このクラスは、次のようにしました。

           			
        PagingModel
              └ ConditionAndMaterials
           			
           			

 PagingModel がページングに必要な機能を網羅した汎用クラスです。 ConditionAndMaterials クラスは派生クラスとなり、ページングの機能をそのまま利用するクラスです。 そして、このクラスはフィールドワークデータを検索した後のビューで使用するモデルとなります。ページングが必要なビューでは、PagingModel から派生したクラスをモデルとするだけで、ページングの機能を利用できるようになります。

 ただし、条件があります。あまりないとは思いますが、1画面で異なるページングデータを複数操作したい場合は、継承を利用したこのやり方では対応できません。1画面で利用できるページングは1つに限定されます。 もちろん、テーブルの上下にそれぞれページング用ボタンなどを配置するのはかまいません。全く同じデータをもとにしてページングしているだけですので、問題なく動作します。
 異なるページングを複数利用したい場合はどうするか?そのためには、必要なだけの PagingModel を変数にもつクラスを定義して、それぞれの PagingModel 変数に処理を委譲するようにする必要があるでしょう。工夫してみてください。

 PagingModel の実装を示します。豊富なコメントが付いていますので、プログラムは長いですが、理解は難しくないと思います。

           			
1     public class PagingModel
2     {
3         private static int PAGEITEMS = 10;

4         public PagingModel()
5         {
6             numbers = null;
7             Mode = ModeValue.ALL;
8             Count = 0;
9             Page = 0;
10            PageItems = PAGEITEMS;
11            NumberItems = PAGEITEMS;
12            Url = "";
13            Param = "page";
14        }

        /*
         ページングで使用するナビゲーション用のボタンとリンク
         デフォルトでは、ALL
        FIRST   先頭ページに移動するボタン
        PREV    前頁に移動するボタン
        NEXT    次頁に移動するボタン
        LAST    最終ページに移動するボタン
        NUMBER  ページ番号で直接移動するページ番号
        ALL     全てのナビゲーション用ボタンとリンクを有効にする
        */
15      public enum ModeValue { FIRST = 0x01, PREV = 0x02, NEXT = 0x04, LAST = 0x08, NUMBER = 0x10, ALL = FIRST | PREV | NEXT | LAST | NUMBER };

        /*
        現在のナビゲーションボタンとリンク
        デフォルトではALL
        */
16      public ModeValue Mode { get; set; }

        /*
        データの総数
        */
17      public int Count { get; set; }
        
        /*
        1ページに表示するデータの件数
        デフォルトでは10件
        */
18      public int PageItems { get; set; }

        /*
        表示モードにNUMBERが含まれている場合に、何件のページ番号を表示するか
        デフォルトではページ番号は10件
        */
19      public int NumberItems { get; set; }

        /*
        リンク先のURL
        */
20      public string Url { get; set; }

        /*
        ページ番号をGETで指定するためのパラメータ名
        paramがpageであった場合、?page=1のような書式でURLが呼び出される
        デフォルトのパラメータ名は「"page"」
        */
21      public string Param { get; set; }

        /*
        GETのパラメータで渡されたページ番号にバインドする変数
        ページは1を基準にする
        */
22      public int Page { get; set; }

23      public IEnumerable<Object> Objects
24      {
25          get; set;
26      }

27      public string FirstUrl()
28      {
29          return Url + "?" + Param + "=1";
30      }

31      public string PrevUrl()
32      {
33          if (Page < 1)
34              return "";
35          int index = Page - 1;
36          if (index <= 0)
37              index = 1;

38          return Url + "?" + Param + "=" + index;
39      }

40      public string NextUrl()
41      {
42          if (Page < 1)
43              return "";

44          int index = Page - 1;
45          index += 2;
46          int max = Count / PageItems;
47          if (Count % PageItems != 0)
48              ++max;

49          index = index > max ? max : index;
50          if (index <= 0)
51              ++index;

52          return Url + "?" + Param + "=" + index;
53      }

54      public string LastUrl()
55      {
56          int max = Count / PageItems;
57          if (Count % PageItems != 0)
58              ++max;
59          if (max <= 0)
60              ++max;

61          return Url + "?" + Param + "=" + max;
62      }

63      public string NumberUrl(int val)
64      {
65          return Url + "?" + Param + "=" + val.ToString();
66      }

67      private List<int> numbers;
68      public List<int> Numbers()
69      {
70          if (Count <= 0 || PageItems <= 0 || NumberItems <= 0 || (Mode & ModeValue.NUMBER) == 0)
71              return new List<int>();

72          if (numbers != null)
73              return numbers;

74          int pages = Count / PageItems;
75          if (Count % PageItems != 0)
76              ++pages;

77          int start, current = Page, half = NumberItems / 2;
78          if (current <= half)
79              start = 1;
80          else if (current + half > pages)
81              start = pages - NumberItems + 1;
82          else
83              start = current - half;

84          List<int> values = new List<int>();
85          for (int i = 0; start <= pages && i <= NumberItems - 1; ++i, ++start)
86              values.Add(start);

87          numbers = values;
88          return numbers;
89      }

90      public bool IsMode(ModeValue mode)
91      {
92          return ((int)mode & (int)Mode) != 0;
93      }

94      public string IsModeString(ModeValue mode)
95      {
96          return IsMode(mode) ? "" : "hidden";
97      }

98      public bool IsValid()
99      {
100         return Count > 0;
101     }

102     public string IsValidString()
103     {
104         return IsValid() ? "visible" : "hidden";
105     }

106     public string IsPageString(int num)
107     {
108         return num == Page ? "text-danger" : "text-primary";
109     }

110     public void Paging(int startPage, EnumerableTable table)
111     {
112         Page = 0;
113         Count = table.GetRows();
114         if (Count <= PageItems)
115         {
116             Page = 1;
117             Objects = table.ToEnumerable();

118             return;
119         }

120         int index = 0;
121         int cnt = Count;
122         DataRow row;
123         for (int max = (startPage - 1) * PageItems; index < cnt && index < max; ++index)
124         {
125             row = table.GetRow(index);
126             row.Delete();
127         }

128         index += PageItems;
129         for (; index < cnt; ++index)
130         {
131             row = table.GetRow(index);
132             row.Delete();
133         }

134         table.DataTable.AcceptChanges();

135         Page = startPage;
136         Objects = table.ToEnumerable();
137     }
138 }
           			

 3行目:1ページに表示する項目数のデフォルト値です。デフォルトでは1ページに10件のデータを表示します
 4~14行目:コンストラクタです
 15~22行目:各種のプロパティです。内容はコメントやプロパティ名から自明と思います
 23~26行目:現在のページ内容となるデータを示します。Paging(int, EnumerableTable) メソッドで計算されます
 27~66行目:ページを切り替えるボタンに対応したメソッドです
 68~89行目:表示に使用するページ番号を返すメソッドで、以下で説明します
 110~137行目:開始ページ番号と検索結果の全データから、そのページに該当するデータだけを抽出するメソッドです。このメソッド呼び出しが必ず必要です

 PagingModel クラスの中で分かりにくいのは、 Numbers() メソッドだと思います。このメソッドは、ページ番号を直接クリックして遷移する画面を作成するときに、 どのページ番号から、どのページ番号までが必要となるかを計算するメソッドになります。
 説明が簡単となるように、次の条件で図解します。全ページ数が9ページとします。1つの画面には、1度に最大5ページ分(デフォルトでは10です)の数字が並ぶものとします。
 以下の図では、その5ページ分のページ番号が[ ]で囲まれています。そして、今表示されているページ番号が( )で囲まれています。 今表示されているページが1 → 2 → … → 9と変化していくと、計算される5ページ分のページ番号がどのようになっていくのかを示しています。
 Numbers() メソッドはこの[ ]で囲まれている数字のリストを計算するメソッドになります。

           			
          [ (1)  2   3   4   5 ] 6   7   8   9
          [  1  (2)  3   4   5 ] 6   7   8   9
          [  1   2  (3)  4   5 ] 6   7   8   9
             1 [ 2   3  (4)  5   6 ] 7   8   9
             1   2 [ 3   4  (5)  6   7 ] 8   9
             1   2   3 [ 4   5  (6)  7   8 ] 9
             1   2   3   4 [ 5   6  (7)  8   9  ] 
             1   2   3   4 [ 5   6   7  (8)  9  ] 
             1   2   3   4 [ 5   6   7   8  (9) ] 
           			
           			

 1ページに表示するページ番号は最大5個ですから、5個の数字のリストを返します。しかし、全ページ数9ページの最初と最後では、返す数字のリストは 変わりませんが、現在のページ番号は変化していくことになります。

ページング用部分ビュー

 ページング用のビューはモデルと比較すると単純です。

           			
      _PagingView.cshtml のソース
1     @model FieldworkForMVC.Models.PagingModel

2     <div class="text-center @Model.IsValidString()">
3         <a type="button" href="@Model.FirstUrl()" class="btn btn-default @Model.IsModeString(FieldworkForMVC.Models.PagingModel.ModeValue.FIRST)">
4             <span class="glyphicon glyphicon-fast-backward"></span>
5         </a>
6         <a type="button" href="@Model.PrevUrl()" class="btn btn-default @Model.IsModeString(FieldworkForMVC.Models.PagingModel.ModeValue.PREV)">
7             <span class="glyphicon glyphicon-step-backward"></span>
8         </a>
9         <span style="margin: 5px;font-size: 1.5em;">
10            @foreach (var item in Model.Numbers())
11            {
12                <a href="@Model.NumberUrl(item)" class="@Model.IsPageString(item)">@item.ToString()</a>
13            }
14        </span>
15        <a type="button" href="@Model.NextUrl()" class="btn btn-default @Model.IsModeString(FieldworkForMVC.Models.PagingModel.ModeValue.NEXT)">
16            <span class="glyphicon glyphicon-step-forward"></span>
17        </a>
18        <a type="button" href="@Model.LastUrl()" class="btn btn-default @Model.IsModeString(FieldworkForMVC.Models.PagingModel.ModeValue.LAST)">
19            <span class="glyphicon glyphicon-fast-forward"></span>
20        </a>
21    </div>
           			
           			

 1行目:モデル PagingModel の指定です
 2行目:ページングそのものを表示するかどうかの判定です
 3~5行目:先頭ページに移動の部分です。「<a href="ターゲットのURL?page=1">先頭のアイコン</a>」を出力するのが目的です
 6~8行目:前ページに移動の部分です。「<a href="ターゲットのURL?page=前ページ番号">前項のアイコン</a>」を出力するのが目的です
 9~14行目:ページ番号で直接移動する部分です。「<a href="ターゲットのURL?page=ページ番号">数字</a>」を出力するのが目的です
 15~17行目:次ページに移動の部分です。「<a href="ターゲットのURL?page=次ページ番号">次項のアイコン</a>」を出力するのが目的です
 18~20行目:最終ページに移動の部分です「<a href="ターゲットのURL?page=最終ページ番号">最終のアイコン</a>」を出力するのが目的です


 この部分ビューを利用する方法を示します。利用は、2段階の部分ビューの階層となっています。

           			
        _PagingView.cshtml
              └ _MaterialView.cshtml
                       └ Search.cshtml
           			
           			

 _PagingView は PagingModel を直接使用するビューです。 _MaterialView は部分ビューとして _PagingView を使用します。 フィールドワークデータのリストに該当するテーブルの上と下にページングの部分を部品として貼り付けたビューになります。
 Search はフィールドワークデータの検索後の画面に相当するビューになり、 _MaterialView を部品として貼り付けたビューになります。

 ここで、 _MaterialView をなくすことも可能です。ただし、一覧のテーブル部分を部品化しておくことで、再利用性が高まると考えて、このような構成をとりました。

 さて、 Search と _MaterialView はモデルとして、 ConditionAndMaterials を利用します。一方、 _PagingView が使用する モデルは PagingModel です。しかし ConditionAndMaterials クラスは PagingModel から派生していますので、このままで正しく動作します。

           			
      _MaterialView の一部分
1     @model FieldworkForMVC.Models.ConditionAndMaterials

2     @Html.Partial("_PagingView")
3     <table class="table table-condensed table-bordered p100">
          略
4     </table>
5     @Html.Partial("_PagingView")
           			
           			

 1行目:モデルを指定しています
 2行目:テーブルの上のページング部分です
 3~4行目:テーブルの本体部分です
 5行目:テーブルの下のページング部分です

 ページングが必要な3行目のテーブルタグの上下に、ページング用の部分ビューを配置してあります。Search ビューでは、この部分ビューをさらに利用しているだけです。

一覧画面-一般編

 フィールドワークデータの検索画面を検索前の画面(下記図面3)-状態遷移図の初期状態-と検索後の画面(下記図面4)-状態遷移図の検索-を示します。 (画像をクリックすると、拡大表示します)

画面3
画面4

 以下では、次の項目に関して議論します。

  • クラス構成
  • コントローラクラス
  • ビュークラス
    • 検索前のビュー
      状態移図の初期状態に該当します
    • 検索後のビュー
      状態遷移図の検索後に該当します

クラス構成

  • MaterialsController
    以下のメソッドがあります。設計上の詳細は こちら を参照してください
    • Index GET用
      初期状態になる場合に呼び出されます
    • Index POST用
      初期状態から「追加」「検索」「クリア」ボタンのクリックで呼び出されて、それぞれの状態に遷移します
    • Create GET用
      初期状態から「追加」ボタンのクリックで遷移した場合に呼び出されます
    • Create POST用
      追加状態から「保存」ボタンのクリックで呼び出されて、初期状態に遷移します
    • Search GET用
      初期状態から「検索」ボタンのクリックで遷移した場合に呼び出されます
    • Search POST用
      検索後の状態から「更新」「参照」「削除」ボタンのクリックで呼び出されて、それぞれの状態に遷移します
    • Refer GET用
      検索後の状態から「参照」ボタンのクリックで遷移した場合に呼び出されます
    • Refer POST用
      参照状態から「戻る」ボタンのクリックで呼び出されて、初期状態に遷移します
    • Update GET用
      検索後の状態から「更新」ボタンのクリックで遷移した場合に呼び出されます
    • Update POST用
      更新状態から「保存」ボタンのクリックで呼び出されて、初期状態に遷移します
    • Delete GET用
      検索後の状態から「削除」ボタンのクリックで遷移した場合に呼び出されます
    • Delete POST用
      削除状態から「削除」ボタンのクリックで呼び出されて、初期状態に遷移します
  • ビュー
    コントローラに対応して、以下のビューが必要です
    • Index.cshtml
      初期状態の画面に対応します
    • Create.cshtml
      追加状態の画面に対応します
    • Search.cshtml
      検索後の画面に対応します
    • Refer.cshtml
      参照状態の画面に対応します
    • Update.cshtml
      更新状態の画面に対応します
    • Delete.cshtml
      削除状態の画面に対応します
    • _MaterialView.cshtml
      フィールドワークデータの一覧を表示する部分ビューです
      ページング部分の表示を行う部分ビュー _PagingView.cshtml を利用しています
  • 問題領域クラス
    モデルクラスのうち、問題領域のデータを表現したクラスです
    • Area
      エリアを表現したクラスです
    • ConditionAndMaterials
      フィールドワークデータの検索条件と検索結果のリストを管理するためのクラスです
    • Image
      イメージファイルを表現したクラスです
    • Major
      大分類データを表現したクラスです
    • Material
      フィールドワークデータを表現したクラスです
    • MaterialWindowCondition
      フィールドワークデータの検索条件を表現したクラスです
    • Minor
      小分類を表現したクラスです
    • Polygon
      地図上の多角形を表現したクラスです
    • Position
      地図上の緯度経度を表現したクラスです
    • Unit
      単位データを表現したクラスです
  • データベースクラス
    モデルクラスのうち、データベース関連に対応したクラスです
    • AreaTable
      エリアテーブルに対応したクラスです
    • FieldworkDB
      今回使用するデータベースクラスです
    • ImageTable
      イメージテーブルに対応したクラスです
    • MajorTable
      大分類テーブルに対応したクラスです
    • MaterialTable
      フィールドワークテーブルに対応したクラスです
    • MinorTable
      小分類テーブルに対応したクラスです
    • UnitTable
      単位テーブルに対応したクラスです

MaterialController -Index GET用

 初期状態に入ってくるメソッドの実装を示します。

					
      MaterialController の一部分
1     public ActionResult Index()
2     {
3         ReadTable();
4         ViewBag.Majors = majorTable.ToSelectList();
5         ViewBag.Minors = new List<SelectListItem>();
6         ViewBag.Areas = areaTable.ToSelectList();

7         MaterialWindowCondition condition = new MaterialWindowCondition();
8         return View(condition);
9     }
					

 1行目:Index GET用のメソッド定義です
 3行目:初期画面に必要な、大分類・小分類・エリア・単位の各テーブルを読み出します
 4行目:大分類データからコンポボックス用データに変換します。詳細は こちら を参照してください
 5行目:小分類用コンポボックスを空状態にします(大分類が未選択なので、小分類は選択できません)
 6行目:エリアデータからコンボボックス用データに変換します。詳細は こちら を参照してください
 7行目:検索条件のインスタンスを生成します(初期状態なので、条件は空状態です)
 8行目:初期状態のビューを返します

MaterialController -Index POST用

 初期状態から出ていくメソッドの実装を示します。

					
      MaterialController の一部分
1     [HttpPost]
2     public ActionResult Index(string button, FormCollection collection)
3     {
4         if (button == null)
5         {
6             string mid = collection.GetValue("majorId").AttemptedValue;
7             ReadTable(mid);

8             MaterialWindowCondition condition = ToCondition(collection);
9             ViewBag.Majors = majorTable.ToSelectList(condition.MajorId);
10            ViewBag.Minors = minorTable.ToSelectList();
11            ViewBag.Areas = areaTable.ToSelectList(condition.AreaId);

12            return View(condition);
13          }
14          else if (button.Equals("追 加"))
15          {
16              return RedirectToAction("Create");
17          }
18          else if (button.Equals("検 索"))
19          {
20              MaterialWindowCondition condition = ToCondition(collection);
21              Session["FieldworkCondition"] = condition;

22              return RedirectToAction("Search");
23          }

24          return RedirectToAction("Index");
25      }
					

 1行目: POST の宣言です
 2行目:メソッドの定義です。第一引数でクリックされたボタンを、第二引数で画面項目の値を受け取ります
 4行目:ボタンが無効な場合(大分類が変更されてポストバックされたとき)の処理です
 6行目:選択された大分類の値を知ります
 7行目:選択された大分類を指定して、大分類;小分類・エリア・単位テーブルを検索します
 8行目:ブラウザの画面をもとに、検索条件を作成します
 9行目:大分類の値を指定して、大分類用コンボボックスのリストを作成します
 10行目:小分類用コンポボックスのリストを作成します
 11行目:エリア用コンポボックスのリストを作成します
 12行目:初期状態のビューを返します
 14行目:追加ボタンがクリックされた場合の処理です
 16行目:追加画面に遷移します
 18行目:検索ボタンがクリックされた場合の処理です
 20行目:ブラウザの画面をもとに、検索条件を作成します
 21行目:検索条件をセッションに格納します。次の画面で検索条件が必要になるためです
 22行目:検索後の画面に遷移します
 24行目:初期状態に遷移します(クリアボタンが押された場合です)

MaterialController -Search GET用

 検索後の状態に入ってくるメソッドの実装を示します。

					
      MaterialController の一部分
1     public ActionResult Search()
2     {
3         Session["selectedId"] = "";

4         string val = Request.QueryString.Get("page");
5         int page = 1;
6         if (! string.IsNullOrEmpty(val))
7             int.TryParse(val, out page);

8         MaterialWindowCondition condition = (MaterialWindowCondition)Session["FieldworkCondition"];
9         string mid = condition.MajorId;

10        ReadTable(mid);
11        ViewBag.Majors = majorTable.ToSelectList(mid);
12        ViewBag.Minors = minorTable.ToSelectList(condition.MinorId);
13        ViewBag.Areas = areaTable.ToSelectList(condition.AreaId);

14        materialTable = new MaterialTable(database);
15        if (! materialTable.Search(condition))
16        {
17            ModelState.AddModelError("dbErrorMaterial", minorTable.Result);
18        }
19        else if (! string.IsNullOrEmpty(condition.AreaId))
20        {
21            Area area = areaTable.ToObject(condition.AreaId);
22            Position point;

23            Polygon polygon = new Polygon(area.Positions);

24            DataRow row;
25            Material item;
26            String lat, lon;
27            for (int i = 0, cnt = materialTable.GetRows(); i < cnt; ++i)
28            {
29                row = materialTable.GetRow(i);
30                item = materialTable.ToObject(row);
31                lat = item.Latitude;
32                lon = item.Longitude;
33                if ((!String.IsNullOrEmpty(lat)) && (!String.IsNullOrEmpty(lon)))
34                {
35                    point = new Position(lat, lon);

36                    if (!polygon.IsInclude(point))
37                        row.Delete();
38                }
39                else
40                {
41                    row.Delete();
42                }
43           }

44           materialTable.DataTable.AcceptChanges();
45       }

46       ViewBag.PhotoModels = GetPhotoModels(materialTable);

47       Material material = new Material();
48       ConditionAndMaterials model = new ConditionAndMaterials(condition, material);
49       model.Url = "Search";
50       if (page >= 1)
51           model.Paging(page, materialTable);

52       return View(model)
53    }
					

 1行目:メソッドの定義です
 3行目:検索結果で選択されている項目が無い状態にします
 4~7行目:URL のパラメータからページ番号を得ます。ページング指定で、ページ番号を指定される場合があるためです
 8~9行目:直前の検索条件をセッションから読み出します
 11~13行目:画面のコンポボックスで使用するリストを用意します
 15行目:検索に失敗した場合です
 19行目:検索条件にエリアを指定した場合です。エリアは SQL で実行することができず、コードで検査することが必要です
 21~44行目:エリアの条件の検査を実施し、該当するデータだけを検索結果に残します
 46行目:検索条件に該当するすべての写真データを集めてきます
 47~48行目:ビュー用のモデルを生成します
 49行目:ページング用のビューの名前を登録しますk
 50~51行目:すべてのデータから現在のページ分のデータだけを抽出します
 52行目:検索後のビューを返します

MaterialController -Search POST用

 検索後の状態から出ているくメソッドの実装を示します。

					
      MaterialController の一部分
1         [HttpPost]
2      public ActionResult Search(string button, FormCollection collection)
3      {
4          if (button == null)
5          {
6              string mid = collection.GetValue("majorId").AttemptedValue;
7              ReadTable(mid);

8              MaterialWindowCondition condition = ToCondition(collection);
9              ViewBag.Majors = majorTable.ToSelectList(condition.MajorId);
10             ViewBag.Minors = minorTable.ToSelectList();
11             ViewBag.Areas = areaTable.ToSelectList(condition.AreaId);
12         }
13         else if (button.Equals("追 加"))
14         {
15             return RedirectToAction("Create");
16         }
17         else if (button.Equals("参 照"))
18         {
19             Session["selectedId"] = collection.GetValue("selectedId").AttemptedValue;
20             return RedirectToAction("Refer");
21         }
22         else if (button.Equals("更 新"))
23         {
24             Session["selectedId"] = collection.GetValue("selectedId").AttemptedValue;
25             return RedirectToAction("Update");
26         }
27         else if (button.Equals("削 除"))
28         {
29             Session["selectedId"] = collection.GetValue("selectedId").AttemptedValue;
30             return RedirectToAction("Delete");
31         }

32         return RedirectToAction("Index");
33     }
					

 1行目: POST の宣言です
 2行目:メソッドの定義です。第一引数でクリックされたボタンを、第二引数で画面項目の値を受け取ります
 4行目:ボタンが無効な場合(大分類が変更されてポストバックされたとき)の処理です
 6行目:選択された大分類の値を知ります
 7行目:選択された大分類を指定して、大分類;小分類・エリア・単位テーブルを検索します
 8行目:ブラウザの画面をもとに、検索条件を作成します
 9行目:大分類の値を指定して、大分類用コンボボックスのリストを作成します
 10行目:小分類用コンポボックスのリストを作成します
 11行目:エリア用コンポボックスのリストを作成します
 13行目:追加ボタンがクリックされた場合の処理です
 15行目:追加画面に遷移します
 17行目:参照ボタンがクリックされた場合の処理です
 19行目:選択状態のフィールドワークデータのIDを記録します
 20行目:参照画面に遷移します
 22行目:更新ボタンがクリックされた場合の処理です
 24行目:選択状態のフィールドワークデータのIDを記録します
 25行目:更新画面に遷移します
 27行目:削除ボタンがクリックされた場合の処理です
 29行目:選択状態のフィールドワークデータのIDを記録します
 30行目:削除画面に遷移します
 32行目:初期画面に遷移します

Index.cshtml -コンボボックスの実装

  Index.cshtml はフィールドワークデータを操作する初期状態に対応したビューです。このビューには、大分類コンポボックスがあります。大分類コンボボックスを変更すると、小分類コンボボックスは その大分類に所属する小分類だけに限定される必要があります。
 この仕様満たすためには、少なくとも3種類の実装が考えられます。以下、順番に議論したいと思います。最初の実装は、大分類のコンボボックスを変更すると、 Web サーバにサブミットするものです。

           			
      Index.cshtml の一部分
1     <script>
2         $(function () {
3             $("#majorId").change(function () {
4                 $("#mainForm").submit();
5             });
6         });
7     </script>
8     >body<
          略
9         @using (Html.BeginForm("Index", "Materials", FormMethod.Post, new { id = "mainForm", @class = "form-horizontal", @role = "form" }))
10        {
              略
11            @Html.DropDownList("majorId", (IEnumerable<SelectListItem>)ViewBag.Majors, " -------------------- ", new { @class = "form-control" })
              略
12        }
13    </body>      
           			
           			

 この処理の中心は3~4行目になります。大分類コンボボックス(majorId を id 属性に持ちます)の値が変更されたときに、 form をサブミットしています。 ここで <form> タグを見ましょう。 method 属性に post を指定しています。そのため、このやり方では POST で Web サーバにサブミットされることになります。 このままでは、サーバ側で対処しない限り Post-Redirect-Get に違反する遷移となります。たとえ違反したとしても、実害は起こりませんから、どう考えるかに依存しそうです。
 しかし、次のように GET に変更することができます。これが、2つ目の実装になります。

           			
      Index.cshtml の一部分
1     <script>
2         $(function () {
3             $("#majorId").change(function () {
4                 $("#mainForm").attr("method", "get");
5                 $("#mainForm").submit();
6             });
7         });
8     </script>
					
					

 4行目で、jQury を使用して、動的に<form> タグの method 属性を get に書き換えています。こうすることで通常は POST でサブミットするけれども、大分類のコンボボックスが変更された場合だけ、 GET によるサブミットに変更できます。
 ただし、この方法は GET のパラメータ長に依存するという問題はあります。今回程度のパラメータ長であれば問題ありませんが、余りにも長いパラメータの場合には、 ブラウザや Web サーバなどのどこかで GET のパラメータが打ち切られる可能性があります。

 3つ目の実装は、Web サーバにサブミットせずに、 Ajax を使用して小分類のコンボボックス部分だけを非同期更新するというものです。 Web サーバにサブミットすると画面全体を再描画することになりますから、ブラウザの画面がごく短い時間ですがチラツキます。再描画の必要な部分だけ表示を更新すれば、 画面のチラツキを最小限に抑えることができます。この場合のサンプルは省略します。次のような用意が必要になります。

  • 大分類変更を受信するハンドラが必要
    ブラウザからの大分類の変更の通知を受信して、該当する小分類を検索し、その検索結果を返すハンドラが Web サーバに必要です
    ハンドラの例は、 こちら を参照してください
  • Ajax プログラムが必要
    大分類の変更時にサブミットするのではなく、上記のハンドラと通信し、その戻り値で小分類コンボボックスを更新する処理が必要です
    Ajax の処理に関しても同じく、 こちら を参照してください
 3つの実装方法のうち、どの方法を採用するかを検討する必要があります。

追加画面

 フィールドワークデータの追加画面を次に示します。(画像をクリックすると、拡大表示します)

画面5

MaterialController -Create GET用

 追加状態に入ってくるメソッドの実装を示します。

					
     MaterialController の一部分
1    public ActionResult Create()
2    {
3        Session["NewFieldworkPointMVC"] = "";

4        ReadTable();
5        ViewBag.Majors = majorTable.ToSelectList();
6        ViewBag.Minors = new List<SelectListItem>();
7        ViewBag.Units = unitTable.ToSelectList();

8        Material material = new Material();
9        return View(material);
10    }
					

 1行目:メソッドの定義です
 3行目:フィールドワークデータの緯度経度を未入力にします
 4~7行目:画面のコンポボックスで使用するリストを用意します
 8行目:ビュー用のモデルを生成します
 9行目:追加画面用のビューを返します

MaterialController -Create POST用

 追加状態から出ていくメソッドの実装を示します。

					
     MaterialController の一部分
1    [HttpPost]
2    public ActionResult Create(string button, FormCollection collection, HttpPostedFileBase[] fileUpload)
3    {
4        Material material = ToMaterial(collection);

5        if (button == null)
6        {
7            material.MinorId = "";
8            ReadTable(material.MajorId);
9            ViewBag.Majors = majorTable.ToSelectList(material.MajorId);
10           ViewBag.Minors = minorTable.ToSelectList();
11           ViewBag.Units = unitTable.ToSelectList(material.UnitId);

12           return View(material);
13       }
14       else if (button.Equals("保 存"))
15       {
16           database = new FieldworkDB();
17           materialTable = new MaterialTable(database);
18           imageTable = new ImageTable(database);

19           int id = -1;
20           string sql = materialTable.AppendSql(material);

21           realAction = (() =>
22           {
23               if ((id = database.ExecuteThenReadId(sql)) < 0)
24               {
25                   throw new Exception(database.Result);
26               }

27               string lat = "", lon = "";
28               UploadImages(ref lat, ref lon, id, imageTable, fileUpload);

29               if (string.IsNullOrEmpty(material.Latitude) &&
30                   string.IsNullOrEmpty(material.Longitude) &&
31                   ! string.IsNullOrEmpty(lat) &&
32                   ! string.IsNullOrEmpty(lon))
33               {
34                   if (database.Execute(materialTable.UpdateLatLonSql(id, lat, lon)) < 0)
35                   {
36                       throw new Exception(database.Result);
37                   }
38               }
39           });

40           Execute();

41           if (id < 0)
42           {
43               return RedirectToAction("Index");
44           }
45       }
46       else if (button.Equals("クリア"))
47       {
48           return Create();
49       }

50       return RedirectToAction("Index");
51   }

					

 1行目: POST の宣言です
 2行目:メソッドの定義です。第一引数でクリックされたボタンを、第二引数で画面項目の値を、第三引数で添付ファイルの情報を受け取ります
 4行目:ブラウザの画面からフィールドワークデータを作成します
 5行目:ボタンが無効な場合(大分類が変更されてポストバックされたとき)の処理です
 7~11行目:画面のコンボボックス用リストを用意します
 12行目:追加画面のビューを返します
 14行目:保存ボタンがクリックされたときの処理です
 16~18行目:データベースの準備です
 20行目:追加用の SQL の準備です
 21行目:トランザクションの本体を格納するラムダ式を用意します
 23行目:フィールドワークデータの追加に失敗した場合です
 25行目:トランザクションをロールバックするために例外を投げます
 28行目:イメージファイルの情報をデータベースに格納します。エラーがあった場合、その別メソッド中で例外を投げます
 29~32行目:フィールドワークデータの緯度経度が空で、イメージファイルに緯度経度が設定されている場合です
 34行目:イメージファイルの緯度経度でフィールドワークデータを更新し、その更新に失敗した場合です
 36行目:トランザクションをロールバックするために例外を投げます
 40行目:トランザクションを開始するメソッド Execute() を呼び出します。その後、 DoAction() メソッドが呼ばれます。その DoAction() メソッドの内部で、21行目で定義したラムダ式を呼び出しています
 41行目:トランザクションが失敗した場合、初期状態に遷移します


別のプロセスで使用されているため、XXX にアクセスできません

  Visual Studio で動作確認をしていて、タイトルのようなエラーに遭遇することがありました。
 添付ファイルからイメージファイルを指定して保存、そのまま次にそのイメージファイルを削除する処理を繰り返すと、このエラーが発生します。

 このエラーはファイルを保存したプロセスがそのままファイルハンドルを握ったまま解放せず、次に削除の処理を行ったため発生したと考えています。
つまりこのエラーは、デバッグ環境が一体となった Visual Studio の簡易 IIS を使用しているためと予想します。 Web アプリケーションの性格から、Web サーバはブラウザに 応答を返すと、使用したリソースはすべて解放するのが仕様です。つまり、実運用下では発生しないはずです。

 もし、デバッグ時にこのエラーに遭遇した場合は、いったんデバッグ用に起動したブラウザを終了すると回避することができます。 しかしながら、本質的にこの問題を解決する方法は見つかっていません。