このプログラムの開発の経緯は こちら にも書きましたが、Windows WPF → Web Form → Web MVC の
順で作成してきました。モデルクラスは、現在注目している分野を構成するクラスですから、どのようなフレームワークを使用しているかとは、本来別の位置にあるクラスです。
実際に、最初に開発した Windows WPF 用として作られたモデルクラスは、その基本部分に関してその後の2つのプログラムでも流用できています。
このモデルクラスは、その性格から次の2つのグループとして管理しています。
以下では、この2つのグループに従って、説明していきます。
問題領域のクラスとして、以下のものがあります。これらのクラスは、 1章 のデータベースの説明や 2章 の画面の説明から、容易く了解できると思います。説明の順番はクラス名のアルファベット順です。
クラスの数はごく少ないと思います。
問題領域のクラスは、単純な構造をしています。必要なプロパティを get/set で操作するだけです。プロパティの数が多いか少ないかくらいの差しかありません。
特に、データベースのレコードに対応したクラスは、テーブルのフィールドをそのままプロパティにもっています。
データベースのレコードに対応したクラスの実装例として、 FieldworkModel クラスと Unit クラスの MVC 版を示します。
FieldworkModel はデータベースのレコードに対応したクラスの親となる抽象クラスです。そして、 Unit はその派生クラスの一つです。
単純なクラスですので、説明は省きます。
public abstract class FieldworkModel
{
public string Version { get; protected set; }
[DisplayName("登録日")]
[DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}")]
public DateTime CreateDate { get; set; }
[DisplayName("変更日")]
[DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}")]
public DateTime? ModifyDate { get; set; }
public virtual void Clear()
{
CreateDate = DateTime.Today;
ModifyDate = null;
}
public virtual Boolean IsValid()
{
return true;
}
}
public class Unit : FieldworkModel
{
public Unit()
{
internalClear();
}
[DisplayName("ID")]
public string Id { get; set; }
[DisplayName("名称")]
public string Name { get; set; }
[DisplayName("コメント")]
public string Comment { get; set; }
public override void Clear()
{
base.Clear();
internalClear();
}
public override Boolean IsValid()
{
return base.IsValid() && ! String.IsNullOrEmpty(Id);
}
private void internalClear()
{
Id = string.Empty;
Name = string.Empty;
Comment = String.Empty;
}
}
地図表示に関係する2つのクラス Polygon と Position の実装を以下に示します。データベースのレコードに対応したクラスほど単純ではありませんが、 決して複雑なクラスではありません。
public class Polygon
{
// 多角形の各頂点の位置を収めたリスト
List<Position> polygon_ = new List<Position>();
/// <summary>
/// 多角形の頂点数を返す
/// </summary>
public int Count
{
get
{
return polygon_.Count;
}
}
/// <summary>
/// 指定したインデックスの頂点の緯度経度を返す
/// </summary>
/// Át'param name="index"></param>
/// <returns></returns>
public Position this[int index]
{
get
{
return (index < 0 || index >= polygon_.Count) ? polygon_[0] : polygon_[index];
}
}
/// <summary>
/// 中心の緯度経度を返す
/// 中心座標は、((最小X+ 最大X) / 2, (最小Y + 最大Y) / 2) と定義する
/// </summary>
/// <returns></returns>
public Position Center()
{
Position point = new Position();
if (!IsValid())
return point;
var minX = 0.0m;
var maxX = 0.0m;
var minY = 0.0m;
var maxY = 0.0m;
minX = polygon_.Select(p => p.X).Min();
maxX = polygon_.Select(p => p.X).Max();
minY = polygon_.Select(p => p.Y).Min();
maxY = polygon_.Select(p => p.Y).Max();
point.X = Decimal.Round((minX + maxX) / 2, 14);
point.Y = Decimal.Round((minY + maxY) / 2, 14);
return point;
}
public Polygon() { }
public Polygon(String positions)
{
MakePolygon(positions);
}
public void Clear()
{
polygon_.Clear();
}
// 多角形となるためには、頂点の数は3以上でなければならない
// 頂点が同じ直線上にある場合を考慮しない
public Boolean IsValid()
{
return polygon_.Count() >= 3;
}
// 文字列から多角形を構築する
// 文字列の書式は以下である
// (緯度,経度),(緯度,経度), … ,(緯度,経度)
public void MakePolygon(String positions)
{
polygon_.Clear();
if (String.IsNullOrEmpty(positions))
return;
String[] del0 = { "),", ")" };
String[] del1 = { "," };
String[] points;
// 緯度・経度のペアの数だけ文字列を分割
points = positions.Split(del0, StringSplitOptions.RemoveEmptyEntries);
if (points.Length < 3)
return;
double v0, v1;
String val;
String[] latLon;
Position point;
for (int i = 0, cnt = points.Length; i < cnt; ++i)
{
// 緯度・経度の文字列の処理
val = points[i];
val = val.Trim();
if (val[0] == '(')
val = val.Substring(1);
// 緯度と経度の文字列に分割する
latLon = val.Split(del1, StringSplitOptions.RemoveEmptyEntries);
if (latLon.Length != 2)
{
polygon_.Clear();
return;
}
latLon[0] = latLon[0].Trim();
latLon[1] = latLon[1].Trim();
if (! double.TryParse(latLon[0], out v0) || ! double.TryParse(latLon[1], out v1))
{
// 緯度経度として不正な文字が含まれていた
polygon_.Clear();
return;
}
point = new Position(latLon[0], latLon[1]);
if (! point.IsValid())
{
polygon_.Clear();
return;
}
polygon_.Add(point);
}
}
/// <summary>
/// 自分自身(多角形)の内部に引数の座標が含まれるかどうかを返す
/// </summary>
/// <param name="at"></param>
/// <returns></returns>
public Boolean IsInclude(Position at)
{
Position p1, p2;
bool inside = false;
Position newPoint, oldPoint = this[Count - 1];
for (int i = 0, cnt = Count; i < cnt; ++i)
{
newPoint = this[i];
if (newPoint.X > oldPoint.X)
{
p1 = oldPoint; p2 = newPoint;
}
else
{
p1 = newPoint; p2 = oldPoint;
}
if ((p1.X < at.X) == (at.X <= p2.X) && (at.Y - p1.Y) * (p2.X - p1.X) < (p2.Y - p1.Y) * (at.X - p1.X))
{
inside = !inside;
}
oldPoint = newPoint;
}
return inside;
}
}
// Google map に表示するための緯度・経度を表現したクラス
// 緯度経度とXY座標との変換も行う
public class Position
{
public String Latitude { get; set; }
public String Longitude { get; set; }
public Decimal X
{
get
{
decimal val;
if (Decimal.TryParse(Longitude, out val))
return val;
return 0.0m;
}
set
{
Longitude = "" + value;
}
}
public Decimal Y
{
get
{
decimal val;
if (Decimal.TryParse(Latitude, out val))
return val;
return 0.0m;
}
set
{
Latitude = "" + value;
}
}
public Position()
{
Clear();
}
public Position(String latLon)
{
FromString(latLon);
}
public Position(String latitude, String longitude)
{
Latitude = latitude;
Longitude = longitude;
if (IsValid())
return;
Clear();
}
public void Clear()
{
Latitude = String.Empty;
Longitude = String.Empty;
}
/// <summary>
/// 有効なデータかどうかを返す
/// </summary>
/// <returns></returns>
public Boolean IsValid()
{
if (String.IsNullOrEmpty(Latitude) || String.IsNullOrEmpty(Longitude))
return false;
var x = X;
if (x > 180 || x < -180)
return false;
var y = Y;
return (y > 90 || y < -90) ? false : true;
}
/// <summary>
/// 文字列の緯度経度から、自分自身の値を更新する
/// </summary>
/// <param name="latLon">
/// 書式の例 (38.018365,138.368090)
/// </param>
public void FromString(String latLon)
{
Clear();
if (String.IsNullOrEmpty(latLon))
return;
latLon = latLon.Trim();
if (latLon[0] != '(' || latLon[latLon.Length - 1] != ')')
return;
int index = latLon.IndexOf(',');
if (index < 0)
return;
if (index != latLon.LastIndexOf(','))
return;
String s1 = latLon.Substring(1, index - 1);
String s2 = latLon.Substring(index + 1, latLon.Length - index - 2);
s1 = s1.Trim();
s2 = s2.Trim();
double val;
if (!double.TryParse(s1, out val))
return;
if (!double.TryParse(s2, out val))
return;
Latitude = s1;
Longitude = s2;
if (IsValid())
return;
Clear();
}
public override String ToString()
{
return String.Format("({0},{1})", Latitude, Longitude);
}
}
今回のブログラムでは Entity Framework は使用しません。データベースに関しては、独自クラスを使用します。
その説明は こちら を参照してください。データベースクラスは、この参照先にあるクラスから派生して実装します。
データベースに関係したクラスは、次のものです。説明の順番はクラス名のアルファベット順です。
テーブルに対応したクラスは、どれも同じような実装になっています。つぎに、 UnitTable クラスの Web Form 版の実装を示します。
実は、Web Form と MVC では実装に若干の違いがあります。そのため、ここでは Web Form での実装を示します。その違いの理由は、次章のテーマとなります。
簡単な内容ですし、メソッドの名前から容易に役割が読み取れると思いますので、説明は省略します。
public class UnitTable : Table
{
public UnitTable(Database database)
: base(database)
{
Name = "Unit";
}
public override string AppendSql(Object value)
{
Unit anObj = (Unit)value;
StringBuilder builder = new StringBuilder();
builder.Append("INSERT INTO Unit (ID, Name, Comment) VALUES (");
builder.AppendFormat("'{0}', N'{1}',", anObj.Id, anObj.Name);
if (anObj.Comment.Length > 0)
builder.AppendFormat("N'{0}'", anObj.Comment);
else
builder.Append("''");
builder.Append(");");
return builder.ToString();
}
public override string UpdateSql(System.Data.DataRow row)
{
string ch = "";
StringBuilder builder = new StringBuilder();
builder.Append("UPDATE Unit SET ");
if (row["ID"] != row["ID", System.Data.DataRowVersion.Original])
{
builder.AppendFormat("{0}ID='{1}'", ch, (string)row["ID"]);
ch = ",";
}
if (row["Name"] != row["Name", System.Data.DataRowVersion.Original])
{
builder.AppendFormat("{0}Name=N'{1}'", ch, (string)row["Name"]);
ch = ",";
}
if (row["Comment"] != row["Comment", System.Data.DataRowVersion.Original])
{
builder.AppendFormat("{0}Comment=", ch);
if (row["Comment"] == DBNull.Value)
builder.Append("null");
else
builder.AppendFormat("N'{0}'", (string)row["Comment"]);
ch = ",";
}
builder.AppendFormat(" WHERE ID='{0}' AND Version={1};", (string)row["ID"], (int)row["Version"]);
return builder.ToString();
}
public override string DeleteSql(System.Data.DataRow row)
{
StringBuilder builder = new StringBuilder();
builder.AppendFormat("DELETE Unit WHERE ID='{0}';", (string)row["ID"]);
return builder.ToString();
}
public override bool Read()
{
return Read("select * from Unit where ID <> '-1' order by ID;");
}
public Boolean ReadFromId(string id)
{
StringBuilder builder = new StringBuilder();
builder.Append(GetSql());
builder.AppendFormat(" WHERE ID='{0}';", id);
if (Database.Read(builder.ToString(), this))
return true;
Result = Database.Result;
ErrorCode = Database.ErrorCode;
return false;
}
public void AddDefalutRow()
{
DataRow row = DataTable.NewRow();
row["ID"] = "-1";
row["Name"] = "--------";
DataTable.Rows.InsertAt(row, 0);
}
}
モデルクラスはよく設計されていれば、どのようなフレームワークでも流用できる性格を持っているクラスです。確かに、 Windows WPF と Web Form とでは 流用が良くできています。しかし、 Web MVC では若干の違いがでてきました。その原因は、次の2つが大きいと思います。
それぞれの原因を、以下で議論します。
ただし、この2つに分類できないクラスが1つあります。それは、 PagingModel クラスです。このクラスに関しては こちら を参照してください。 Web Form にはページングの機能が標準で備わっています。しかし、 MVC にはその機能がありません。ページングは自前で用意する必要があります。そのためのモデルクラスになります。
説明のために、単位マスタの画面を示します。(画像をクリックすると、拡大表示します)
ここで、①の部分に該当するモデルは、一つの単位分ですから Unit クラスのインスタンスになります。一方、②に該当するモデルは、現在登録されている単位の一覧ですから Unit クラスのインスタンスのリストとなります。
そこで、この画面全体に対応するモデルを想定すると、次のようなクラスが必要になります。
1 public class UnitAndUnits
2 {
3 public Unit Unit
4 {
5 get; set;
6 }
7 public IEnumerable<FieldworkForMVC.Models.Unit> Units
8 {
9 get; set;
10 }
11 public UnitAndUnits(Unit unit, IEnumerable<FieldworkForMVC.Models.Unit> units)
12 {
13 Unit = unit;
14 Units = units;
15 }
16 }
UnitAndUnits という単純なクラスで、1つの Unit インスタンスと Unit のリストを持ちます。
このクラスは、プログラムの分析や仕様の検討からは見いだせないクラスです。たまたまこのような画面にしようとなって、初めてその必要性に気が付いたものです。
別の画面を想定した場合には、また別のモデルクラスが必要になる可能性もあります。
マスタ系の4つの画面は、ほぼ同様の画面となっています。そこで、次のようなクラスが必要となりました。
さらに、フィールドワークデータを検索して、検索結果を一覧で表示する画面でも、同様のクラスが必要となりました。
これらのモデルクラスは、ビューの画面構成が原因で必要になったクラスです。 MVC でクラス数が増える要因の一つでもあると思います。
Windows Form や Windows WPF さらに Web Form では、データベースからの検索結果を表示するには、標準クラスの DataTable を直接使用できるように設計されています。
一方、 Web MVC では DataTable を使用するようには設計されていません。 DataTable の検索結果を Model クラスのリストに変換する必要があります。
この変換を Entity Framework は自動的にやってくれます。しかし今回は Entity Framework は使用しません。そのため、この変換処理を自前で用意する必要があります。
その変換はどのクラスに実装すべきか?答えは、簡単にでてきます。データベースのテーブルに対応したクラスに実装するのが最適です。
MVC 版の UnitTable では、以下の3つのメソッドが追加されています。 Web Form 版の UnitTable と合わせて確認してください。
1 public Unit ToObject(DataRow row)
2 {
3 Unit unit = new Unit();
4 unit.Id = (string)row["ID"];
5 unit.Name = (string)row["Name"];
6 unit.Comment = row["Comment"] == DBNull.Value ? "" : (string)row["Comment"];
7 unit.CreateDate = (DateTime)row["CDate"];
8 if (row["MDate"] == DBNull.Value)
9 unit.ModifyDate = null;
10 else
11 unit.ModifyDate = (DateTime)row["MDate"];
12 return unit;
13 }
14 public IEnumerable<Unit> ToEnumerable()
15 {
16 List<Unit> list = new List<Unit>();
17 foreach (var row in DataTable.Rows)
18 {
19 list.Add(ToObject((DataRow)row));
20 }
21 return list;
22 }
23 public IEnumerable<SelectListItem> ToSelectList(string selected = "")
24 {
25 DataRow row;
26 SelectListItem item;
27 List<SelectListItem> list = new List<SelectListItem>();
28 for (int i = 0, cnt = GetRows(); i < cnt; ++i)
29 {
30 row = GetRow(i);
31 item = new SelectListItem();
32 item.Value = (string)row["ID"];
33 item.Text = (string)row["Name"];
34 if (selected.Equals(item.Value))
35 item.Selected = true;
36 list.Add(item);
37 }
38 return list;
39 }
1~13行目:1レコードを Unit インスタンスに変換するメソッド
14~22行目:全データを Unit のリストに変換するメソッド
23~39行目:Unit を選択するためのコンボボックスで使用するリストに変換するメソッド
純粋にオブジェクト指向の設計からすると、Web Form 版の UnitTable クラスを定義し、そのクラスから MVC 版の UnitTable を派生して作成するのが望ましいです。
しかし、今回はそのようにはしていません。派生関係を作ってしまうと、2つのプログラム間に依存関係ができてしまいます。 Web Form と MVC を比較する目的からは、そのような依存関係は望ましくありません。
そのため、Web Form の UnitTable のソースプログラムをコピーして3メソッドを追加しました。
そして、もう1組のインターフェース-IEnumerable-とクラス-EnumerableTable-があります。これは、フィールドワークデータをページングしながら表示するために必要になりました。
ページングについては、 こちら を参照してください。フィールドワークテーブルは、 Table クラスから派生するのではなく、
EnumerableTable クラスから派生しています。テーブルのデータをページング機能付きで表示する必要がある場合は、そのテーブルクラスは EnumerableTable クラスから派生する必要があります。
そして、 ToEnumerable() メソッドをオーバーライドしてください。
1 public interface IEnumerable
2 {
3 IEnumerable<Object> ToEnumerable();
4 }
5 public class EnumerableTable : Table, IEnumerable
6 {
7 public EnumerableTable(Database database) : base(database) { }
8 public virtual IEnumerable<Object> ToEnumerable()
9 {
10 return null;
11 }
12 }
1~4行目:ページング機能で使用するインターフェースの定義です
5~12行目:ページング機能を使用するテーブルクラスの親クラスです