7.モデルクラスの実装

初めに

 このプログラムの開発の経緯は こちら にも書きましたが、Windows WPF → Web Form → Web MVC の 順で作成してきました。モデルクラスは、現在注目している分野を構成するクラスですから、どのようなフレームワークを使用しているかとは、本来別の位置にあるクラスです。
 実際に、最初に開発した Windows WPF 用として作られたモデルクラスは、その基本部分に関してその後の2つのプログラムでも流用できています。

 このモデルクラスは、その性格から次の2つのグループとして管理しています。

  • 問題領域クラス
    今取り組んでいる分野に特徴的なクラスです
  • データベース関連クラス
    データベースに関連する性格を持つクラスです
    データベースに関する一般的な説明は こちら をご覧ください

 以下では、この2つのグループに従って、説明していきます。

問題領域クラスの実装

 問題領域のクラスとして、以下のものがあります。これらのクラスは、 1章 のデータベースの説明や 2章 の画面の説明から、容易く了解できると思います。説明の順番はクラス名のアルファベット順です。

  • Area
    エリアテーブルのレコードに対応したクラスです
  • FieldworkModel
    データベースのテーブルのレコードに対応したクラスの親クラスとなる、抽象クラスです
  • Image
    イメージテーブルのレコードに対応したクラスです
  • ImageMarker
    フィールドワークデータを地図に表示するさいのマーカーを表現したクラスです
  • Major
    大分類テーブルのレコードに対応したクラスです
  • Material
    フィールドワークテーブルのレコードに対応したクラスです
  • MaterialWindowCondition
    フィールドワークデータの検索に使用する、検索条件を表現したクラスです
    クラス名に違和感があるとしたら、Windows WPF で作られたものをそのまま流用したからです
  • Minor
    小分類テーブルのレコードに対応したクラスです
  • Polygon
    地図に描く多角形を表現したクラスです
    エリアデータを地図上に描画するためのクラスと考えてください
  • Position
    地図上の緯度経度を表現したクラスです
  • Unit
    単位テーブルのレコードに対応したクラスです

 クラスの数はごく少ないと思います。
 問題領域のクラスは、単純な構造をしています。必要なプロパティを 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);
        }
    }
           			
           			

DBクラスの実装

 今回のブログラムでは Entity Framework は使用しません。データベースに関しては、独自クラスを使用します。 その説明は こちら を参照してください。データベースクラスは、この参照先にあるクラスから派生して実装します。
 データベースに関係したクラスは、次のものです。説明の順番はクラス名のアルファベット順です。

  • AreaTable
    エリアテーブルに対応したクラスです
  • FieldworkDB
    今回使用するデータベースに対応したクラスです
  • ImageTable
    イメージテーブルに対応したクラスです
  • MajorTable
    大分類テーブルに対応したクラスです
  • MaterialTable
    フィールドワークデータテーブルに対応したクラスです
  • MinorTable
    小分類テーブルに対応したクラスです
  • UnitTable
    単位テーブルに対応したクラスです

 テーブルに対応したクラスは、どれも同じような実装になっています。つぎに、 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);
        }
    }
           			
           			

MVC 専用クラス

 モデルクラスはよく設計されていれば、どのようなフレームワークでも流用できる性格を持っているクラスです。確かに、 Windows WPF と Web Form とでは 流用が良くできています。しかし、 Web MVC では若干の違いがでてきました。その原因は、次の2つが大きいと思います。

  • ビューに対応したモデルの必要性
    MVC のモデルはビュークラスと一体となった構造が望ましいためです
  • Entity Framework 不採用のため

 それぞれの原因を、以下で議論します。

 ただし、この2つに分類できないクラスが1つあります。それは、 PagingModel クラスです。このクラスに関しては こちら を参照してください。  Web Form にはページングの機能が標準で備わっています。しかし、 MVC にはその機能がありません。ページングは自前で用意する必要があります。そのためのモデルクラスになります。

ビューに対応したモデルの必要性

 説明のために、単位マスタの画面を示します。(画像をクリックすると、拡大表示します)

画面1

 ここで、①の部分に該当するモデルは、一つの単位分ですから 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 でクラス数が増える要因の一つでもあると思います。

  • AreaAndAreas
    エリアのインスタンス1つと、既存のエリアのリストを管理するクラスです
  • MajarAndMajors
    大分類のインスタンス1つと、既存の大分類のリストを管理するクラスです
  • MinorAndMinors
    小分類のインスタンス1つと、既存の小分類のリストを管理するクラスです
  • UnitAndUnits
    単位のインスタンス1つと、既存の単位のリストを管理するためのクラスです
  • ConditionAndMaterials
    フィールドワークデータの検索条件と、検索結果のフィールドワークデータのリストを管理するためのクラスです
    このクラスの詳細は、 こちら を参照してください

Entity Framework 不採用のため

 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行目:ページング機能を使用するテーブルクラスの親クラスです