空のプロジェクトを作成するまでは、 3章 に記述しました。ここでは、それ以降の作業について議論したいと思います。
その話題は以下のものになります。
Web Form と MVC の2つのプログラムで提供している画面については、 2章 で説明しました。画面数は決して多くはありません。
そして、マスタ系の画面はほぼ同じような見た目と操作方法になっています。また、フィールドワークデータを操作する画面も複雑なものではありません。
そこで、これ以降の実装に関する議論では、以下の画面に限定して説明したいと思います。
Web Form でプログラムを開発する場合、最初に検討するのは、どのような画面がいくつ必要かということです。作業の中心となるのは画面です。 そこで、以下の作業を順に行うことになります。
Web Form でプログラムを作ろうとした場合、上記の Web フォームクラス一つだけでプログラムの記述が可能となります。 C# の部分ですべてのプログラムを書いてしまうというやり方です。 この条件だけが必須であって、これ以外の設計や実装の指針が強制されることはありません。このことは、ややもすればスパゲッティプログラムにつながりかねないといえます。
そこで、次のようなフォルダー階層を導入して、役割を明確にすることにしました。
すべての Web フォームクラスの親となる BasePage クラスのソースは一番上のフォルダーにあります。 BasePage の説明は こちら を参照してください。
MVC と比較した場合、View クラスは Web フォームクラスの html に対応し、 Controller クラスは Web フォームクラスの
プログラム部分に対応すると考えると分かりやすいかもしれません。
Web Form では、 Web フォームクラスは画面部分とプログラム部分が一体となったクラスですので、プログラムの中心となるのはこのクラスであるのは当然ですね。
そしてモデルクラスについては、 Web Form と MVC に関係なく、ほぼ流用が可能となります。これもまた当然の結果ですね。
モデルクラスは、今プログラムを作成しようとしているその分野に対応したクラスのことです。 Web Form や MVC といったサーバー側の実現手法とは独立したクラスになります。
Visual Studio を使用して Web フォームを追加すると、拡張子 aspx を持つファイルが作られます。このファイルにブラウザで表示される html の基となるタグを記述していきます。
以下に、今回の Web Form で使用している aspx ファイルのテンプレートを示します。デフォルトで作成される aspx ファイルとは内容が違っていますので、必要に応じて読み替えや書き換えを行ってください。
1 <code><%@ Page Language="C#" AutoEventWireup="true" CodeBehind="XXX.aspx.cs" Inherits="NNN.XXX" %>
2 <!DOCTYPE html>
3 <html lang="ja">
4 <head id="Head1" runat="server">
5 <meta charset="utf-8">
6 <title></title>
7 <link href="../resource/css/bootstrap.min.css" rel="stylesheet" />
8 <script src="../resource/js/jquery-3.1.0.min.js"<>/script>
9 <script src="../resource/js/bootstrap.min.js"></script>
10 <script>
11 $(function () {
12 });
13 </script>
14 <style type="text/css">
15 </style>
16 </head>
17 <body>
18 </body>
19 </html>
1行目:Web フォームの情報が自動的に生成されます
2行目:html 5 で記述することを指定しています
6行目:タイトルを記述してください
7~9行目:使用する CSS ファイルと javascript ファイルを指定します
10~13行目:jQuery プログラムを記述する場所になります
14~15行目:html 中に直接 CSS を記述する場所になります
17~18行目:この位置に、 html の標準タグや asp: を先頭に持つ特殊タグを書き込んでいきます。特殊タグは Web サーバにより解釈・処理されます。その結果は html の標準タグに変換されてブラウザに返されますので、
どの特殊タグがどのような標準タグに変換されるかは知悉している必要があります
Web フォームを追加すると、拡張子 aspx.cs を持つファイルも同時に作られます。このファイルに C# のプログラムを記述することになります。
以下に、 aspx.cs ファイルのテンプレートを示します。デフォルトの内容とは違っていますし、 import などは省略しています。読み替えや書き換えを行ってください。
1 namespace NNN
2 {
3 public partial class XXX : BasePage
4 {
5 protected void Page_Load(object sender, EventArgs e)
6 {
7 }
8 }
9 }
1行目:名前空間の定義です
2行目:Web フォームクラス名の定義です。 BasePage については こちら を参照してください
5~7行目:ページがロードされた時に呼び出されるハンドラの定義です
7行目以降:この部分に、さまざまなハンドラなどの必要なメソッドを追加していきます。
Page_Load() イベントについて
ページがロードされるときのイベントは Windows Form や Windows WPF でも馴染みのあるイベントです。それは XXX_Loaded (XXX はクラス名)というメソッド名になります。
しかし、このイベントが発生するタイミングが Web アプリケーションと Windows アプリケーションとでは異なります。
Windows アプリケーションの場合、このイベントはそのウィンドウが生成されて、初めて表示されるとき一度だけ発生します。一方、 Web アプリケーションの場合、ブラウザから Web サーバに要求があるたびに発生します。
ブラウザからの要求に対して、 Web サーバが応答をブラウザに返すと、そのあいだの処理に使用したリソースはすべて開放してしまいます。次にブラウザから要求があった場合には、このページを再構築する必要がありますので、
このイベントが発生することになるわけです。
このことは、初めて Web アプリケーションを作成する人が戸惑う点の一つと思います。
初めての要求で発生したイベントなのか、2回目以降の要求によるイベントなのかを調べるには、 IsPostBack を利用することができます。
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// 初めての時だけの処理を記述する
}
}
Web MVC でプログラムを開発するとなった場合、設計や実装の上で一定の制約が課されてきます。また、標準となるフォルダー構成も決まります。
そして、 Web MVC プログラムで中心となるクラスはコントローラクラスになります。
標準のフォルダー構成に準拠したうえで、さらに以下のようなフォルダーを導入することにしました。
MVC の分かりにくさは、 こちら に書きました。ではどのように考えたら、コントローラクラスが分かりやすくなるのか?
そのカギとなるのは状態遷移図だと思います。例があった方が議論が進めやすいので、マスタ系の画面である大分類画面を取り上げます。
その画面と操作方法の詳細については、 こちら を参照してください。画面を再掲します。(画像をクリックすると、拡大表示します)
状態遷移図とは、プログラムがとりうる状態と、その状態間を遷移していく原因を表現したものになります。仕様によってある程度は決まってきますが、 設計によっても変わってきます。そのため、これが正解といったものはありません。以下では、今回 Web MVC で採用した大分類画面の状態遷移図を示します。
状態遷移図の簡単な説明です
これ以降の状態遷移図では、エラー発生時の状態遷移は示さないこととします。エラー発生時には元の状態にとどまります。そうではない例外の場合にだけ、記述することにします。
また、自明と思われる場合は「クリア」による状態遷移も示しません。「初期状態」に遷移すると考えてください。
それでは、この状態遷移図をコントローラクラスの構造に読み替えてみましょう。
まず、状態を2つ持ちますから、以下のメソッドが必要です。ただし、「読出後」の状態を必要としないとすることも可能です。
そうすると、一つの状態でまかなってしまうことになります。 Web Form のプログラムは、状態が一つだけとなっていると考えるとよいかもしれません。
上記の2つの状態は、どちらもその状態に入ってくる矢印と、その状態から出ていく矢印があります。そこで、上記のメソッドはさらに2つに分かれたメソッドが必要になります。
状態遷移図からコントローラの構造が透けて見えてきたと思います。ここまでくれば、必要なビューも自明になるでしょう。 MVC で作成するとなった場合、 設計資料には状態遷移図が欠かせないものになると思います。
コントローラクラスのテンプレートを示します。 import などは省略してありますので、読み替えや書き換えを行ってください。
1 namespace NNN
2 {
3 public class XXX : FieldworkController
4 {
5 // GET: Default
6 public ActionResult Index()
7 {
8 return View();
9 }
10 }
11 }
1行目:このクラスが含まれる名前空間の宣言です
2行目:クラス名の定義です。親クラスとなる FieldworkController については こちら を参照してください
6行目:デフォルトの GET 用初期状態に対応したメソッドです。必要に応じて、メソッド名や処理内容を変更してください
8行目:GET 用初期状態の画面を呼び出しています。必要に応じて変更してください
Web MVC プログラムの中心となるのはコントローラクラスです。ビュークラスを作成するには、コントローラクラスから作成するのが一般的と思います。その手順は、次のようになります。
この操作手順からも、 MVC の中心はコントローラクラスであることが分かると思います。
ビュークラスのテンプレートを示します。必要に応じて、読み替えてください。
1 @model XXX
2 @{
3 Layout = null;
4 }
5 <!DOCTYPE html>
6 <html lang="ja">
7 <head>
8 <meta name="viewport" content="width=device-width" />
9 <title>タイトル</title>
10 <link href="~/Contents/bootstrap.min.css" rel="stylesheet" />
11 <link href="~/Contents/common.css" rel="stylesheet" />
12 <link href="~/Contents/fieldwork-base.css" rel="stylesheet" />
13 <script src="~/Scripts/jquery-3.1.0.min.js"></script>
14 <script src="~/Scripts/bootstrap.min.js"></script>
15 <script src="~/Scripts/bluesky.js"></script>
16 <script>
17 $(function () {
18 });
19 </script>
20 </head>
21 <body>
22 <div>
23 @using (Html.BeginForm())
24 {
25 }
26 </div>
27 </body>
28 </html>
1行目:モデルクラスを指定します。このビューで表示するためのデータを管理しているのがモデルクラスです
5行目:html 5 で記述することを指定します
9行目:タイトルを指定します
10~15行目:CSS と javascript ファイルを指定します
16~19行目:jQuery を記述する場所です
23~26行目:<form> タグの本体を記述する場所です
Post-Redirect-Get(PRG) について
ブラウザから POST 要求がサーバに届いた場合、必要な処理を行って次にどうするか?処理が失敗したならば、そのエラー内容をブラウザに返すことになりますが、
成功した場合は次の遷移先の URL をブラウザに送り返し(Redirect)、ブラウザは新しい URL に対して GET 要求を再度だすようにするのが定石です。
サーバとブラウザ間に一回余分なやり取りが発生しますが、この処理がユーザの操作からは望ましい手順になります。 POST 要求の結果をそのままブラウザに
返した場合、その POST 要求はブラウザの履歴に残ってしまいます。すると、ブラウザの「再読込」や「前ページ」「次ページ」ボタンをクリックするたびに、履歴にある POST 要求が出されてしまいます。
これは望ましくないですね。
さて、ここまでは復習です。 Web Form と MVC とを比較すると、 Web Form では気が付かないうちに Post-Redirect-Get パターン違反を犯しやすい気がします。
とくに気が付きにくいと思われるのは、コンボボックスで項目を選択した場合です。この場合、 Web Form では以下のようにすることが多いでしょう。
<asp:DropDownList ID="XXX" runat="server" AutoPostBack="true"
OnSelectedIndexChanged="XXX_SelectedIndexChanged">
</asp:DropDownList>
asp:DropDownList を AutoPostBack="true" で使用するものです。こうすると、コンボボックスから選択するたびにポストバッグが発生します。 その時に起動される XXX_SelectedIndexChanged メソッドでは、処理結果をそのままブラウザに返すことが多いでしょう。つまり、PRG パターン違反になります。 この場合のパターン違反をどう考えるかは、面倒な問題ですね。これといった実害はないのですが、不可思議なメッセージ(以下に記述)がユーザに提示されるのは望ましくありません。
MVC でも気が付きにくいパターン違反の場合があります。 こちら の状態遷移図において「初期状態」から「追加」で遷移した場合です。 追加処理が失敗した場合は、そのまま遷移する必要がありません。一方、追加が成功した場合は次のように2つの実装があり得ます。
// PRG パターン違反の例
if (処理は成功か?)
{
return Index();
}
// PRG パターンに従った例
if (処理は成功か?)
{
return RedirectToAction("Index");
}
処理が成功した場合に、そのまま初期画面をブラウザに返すのか、一度リダイレクトさせるのかの違いになります。う~ん、めんどくさい。
現在の画面が、PRG パターンに準拠しているかどうかを試すには、ブラウザの「再読込」ボタンをクリックするのが簡単です。 次のメッセージが表示された場合、パターン違反を犯しています。
これはブラウザに Chrome を使用している場合になります。ご使用のブラウザと読み替えてください。
ブラウザの処理は javascript で直接記述することをしません。より記述力が高い jQuery を選択しました。そして、この処理はサーバー側の技術とは関連していません。独立した部分になります。そのため、 Web Form や MVC に限らず、例えば Java で開発した Web アプリケーションでも利用可能です。
ここでは、 jQuery さらには bootstrap を使ってライブラリ的に利用可能な便利な機能を取り上げます。
モーダル画面は bootstrap の機能を利用しています。デフォルトではモーダルダイアログは、画面の上1/3くらいの位置に表示されます。 これを、画面中に表示するには次のようにします。
<!-- jQuery の一部 -->
1 $(function () {
2 $(".modal").on("show.bs.modal", function () {
3 $(this).css("display", "block");
4 var dialog = $(this).find(".modal-dialog");
5 var offset = ($(window).innerHeight() - dialog.height()) / 2;
6 dialog.css("margin-top", offset);
7 });
8 });
注意が必要なのは、2行目:モーダルには「class="modal"」の記述が必要という点だけです。
この方法も bootstrap のモーダルダイアログを利用しています。ブラウザからのサブミット時に、全画面をモーダルダイアログでおおってしまい、サーバーからの応答があるまで 画面操作を禁止することで実現しています。
<!-- jQuery の一部 -->
1 $("#mainForm").submit(function () {
2 $(".modal").hide();
3 $("#hideModal").modal();
4 return true;
5 });
<!-- html の一部 -->
略
6 <form id="mainForm" method="post">
略
7 <div id="hideModal" class="modal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
8 <div class="modal-dialog modal-sm">
9 <div class="modal-content">
10 <div class="modal-body">
11 <img src="~/Contents/loader.gif" alt="" />
12 <strong>Loading ...</strong>
13 </div>
14 </div>
15 </div>
16 </div>
1行目: <form> がサブミットされたときに、ダイアログが表示されます。<form> タグの id は「mainForm」固定です
2行目:表示中のダイアログがあれば表示を消します
3行目:二重押し禁止用のダイアログを表示します
6行目:<form> タグの id に注意
7行目:マウスクリックでモーダル中止を禁止するのが、 data-backdrop="static"。 ESC キー入力でモーダル中止を禁止するのが、 data-keyboard="false"。この2つが重要な部分です
7~16行目:二重押し禁止用のダイアログの本体です
添付ファイルは <input> タグの type="file" で行います。この時の”添付”ボタンの外観を bootstrap のボタンに合わせる方法です。
<!-- html の一部 -->
1 <button class="btn btn-primary" type="button" onclick="$('#fileUpload').click();">ファイルを選択</button>
2 <label id="fileName">選択されていません</label>
3 <input class="hidden" id="fileUpload" multiple="multiple" name="fileUpload" type="file" value="" />
1~2行目:ボタンとラベルを並べて、添付ファイル用の表示を自前で用意する。ボタンクリック時に、 hidden 項目の本来の添付ファイルを起動する
3行目: hidden として消されている添付ファイル項目
ブラウザでエラー検査を行いますが、その際に使用している jQuery の関数を示します。大量のコメントが付いていますので、説明がなくとも理解は容易と思います。
(function ($) {
/*
入力がなにもないかどうかを検査する
空白コードが存在しても,入力があったと判断する
戻り値:
入力がない場合にtrue,そうでない場合にfalse
*/
$.fn.isNull = function () {
return (this.val().length == 0);
};
/*
引数で指定した文字数が入力されているかどうかを検査する
引数:
min 最少文字数
max 最大文字数
戻り値:
必要な文字数が入力されている場合にtrue,そうでない場合にfalse
*/
$.fn.isValidLength = function (min, max) {
var len = this.val().length;
return (len >= min && len <= max);
};
/*
数字キー(0-9)以外のキーが入力されていないかどうかを検査する
入力がない場合も真と判断するため,必須項目の場合は,isNull()と併用すること
戻り値:
数字キーだけまたは空文字列の場合にtrue,そうでない場合にfalse
*/
$.fn.isNumber = function () {
var value = this.val();
if (value.length == 0)
return true;
return !(value.match(/^\d+$/));
};
/*
英文字以外のキーが入力されていないかどうかを検査する
入力がない場合も真と判断するため,必須項目の場合は,isNull()と併用すること
戻り値:
英文字だけまたは空文字列の場合にtrue,そうでない場合にfalse
*/
$.fn.isAlpha = function () {
var value = this.val();
if (value.length == 0)
return true;
return !(value.match(/[^a-zA-Z]/));
};
/*
英数文字以外のキーが入力されていないかどうかを検査する
入力がない場合も真と判断するため,必須項目の場合は,isNull()と併用すること
戻り値:
英数文字だけまたは空文字列の場合にtrue,そうでない場合にfalse
*/
$.fn.isAlphaNumber = function () {
var value = this.val();
if (value.length == 0)
return true;
return !(value.match(/[^0-9a-zA-Z]/));
};
/*
0以上の整数かどうかを検査する
05は整数ではないのでfalseとなる
入力がない場合も真と判断するため,必須項目の場合は,isNull()と併用すること
戻り値:
0以上の整数の場合にtrue,そうでない場合にfalse
*/
$.fn.isPositiveNumber = function () {
var value = this.val();
if (value.length == 0)
return true;
if (!value.match(/^\d+$/))
return false;
if (value.length <= 1)
return true;
return (value.charAt(0) != "0");
};
/*
整数かどうかを検査する
05は整数ではないのでfalseとなる
入力がない場合も真と判断するため,必須項目の場合は,isNull()と併用すること
戻り値:
整数の場合にtrue,そうでない場合にfalse
*/
$.fn.isNumber = function () {
var value = this.val();
if (value.length == 0)
return true;
if (!value.match(/^[-]?[0-9]+(\.[0-9]+)?$/))
return false;
if (value.length <= 1)
return true;
return (value.charAt(0) != "0");
}
/*
正しい日付と判断できるかどうかを検査する
有効な書式は,区切り文字に「-」または「/」を使用した場合,または
年4桁・月2桁・日2桁の8桁の数字を使用し,
日付として有効な組み合わせであること
例:
2014/1/1
2014/1-1
20140228
戻り値:
有効な日付の場合にtrue,そうでない場合にfalse
*/
$.fn.isDate = function () {
return this.getDate() != null;
};
/*
日付に対応するDate型のオブジェクトを返す
有効な日付の書式は,isDate()のコメントを参照せよ
戻り値:
有効な日付の場合にDate型のオブジェクトを,そうでない場合null
*/
$.fn.getDate = function () {
var value = this.val();
if (value.length == 0)
return null;
var a = new Array();
var date = value.replace("-", "/");
if (date.match(/^\d{2,4}\/\d{1,2}\/\d{1,2}$/))
a = date.split("/");
else if (date.match(/^\d\d\d\d\d\d\d\d$/)) {
a[0] = date.substring(0, 4);
a[1] = date.substring(4, 6);
a[2] = date.substring(6, 8);
}
else
return null;
var nd = new Date(a[0], a[1] - 1, a[2]);
if (a[0] == nd.getFullYear() && a[1] == nd.getMonth() + 1 && a[2] == nd.getDate())
return nd;
return null;
};
})(jQuery);