余裕があるうちにCakePHPとWPFの相互運用をまとめていく(準備編) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/5329
の続きで、今度はWPF側からのコーディングです。
いわゆる、以下な感じな、リストビューとプロパティを表示するマスター画面を想定します。
一覧ボタンを押すと、Web APIにつなげて一覧を取得。リストビューの各行をクリックすると右側のプロパティに詳細な値がぽちぽちと表示されるパターンです。このあたり、WPFなので、適当に Binding を使います。最終的には、Visual Studio のプロパティウィンドウのように自動化を目指したいところなのですが…まあ、業務的には各種のプロパティウィンドウをちまちま作っています。というのも、完全に自動化してしまうと使い勝手が悪いことがある(データベース構造に引きずられる)ことになるので、このあたりは適宜カスタムで。
とはいえ、cake bake で自動化できるぐらいには、受け側の WPF のマスター画面も自動化生成しておきたいところです。
■データモデルを作る
まずは、Model を作ります。お店(Store)クラスは、会社(Sellingcompany)と地域(Areagroup)が紐づいているので、Storeクラスの中にプロパティとして入れてしまいます。このあたりの構造は、CakePHPが返すXMLがフラットな構造(Store, Sellingcompany, Areagroupが並列で存在)に対して、モデルのほうはツリー構造になります。IModel.FormXml メソッドは、XElement から適当にモデルにプロパティを入れ込むための簡易メソッドです。業務のほうは ExDoc を使ってみましたが、試しに XElement.Descendants を使ってパース。
- Web APIが返す要素名
- 受け取るときのクラス名
- 受け取るときのプロパティ名
を大文字小文字も合わせて完全に一致させておくと、この方法が取れます。逆に言えば、それぞれの名前が微妙に違ったリスト面倒なことになるわけですが…まあ、そのときは属性を使って回避するのもよし、ここの FromXmlで書き換えるもよし、というところです。
public class IModel { public void FormXml(XElement el) { var pis = this.GetType().GetProperties(); foreach (var pi in pis) { var vv = el.Descendants(pi.Name).FirstOrDefault(); if (vv != null) { var v = vv.Value; if (pi.PropertyType == typeof(int)) pi.SetValue(this, int.Parse(v)); else if (pi.PropertyType == typeof(double)) pi.SetValue(this, double.Parse(v)); else if (pi.PropertyType == typeof(string)) pi.SetValue(this, v); } } } } public class Store : IModel { public int ID { get; set; } public int SellingCompanyID { get; set; } public int AreaGroupID { get; set; } public string Code { get; set; } public string Name { get; set; } public string Person { get; set; } public string Tel { get; set; } public string EMail { get; set; } public string ApplicationProgress { get; set; } public int Enabled { get; set; } public DateTime modified { get; set; } public DateTime created { get; set; } // リンク先 public Areagroup Areagroup { get; set; } public Sellingcompany Sellingcompany { get; set; } public Store(XElement el) { base.FormXml(el); this.Areagroup = new Areagroup(el.Parent.Descendants("Areagroup").FirstOrDefault()); this.Sellingcompany = new Sellingcompany(el.Parent.Descendants("Sellingcompany").FirstOrDefault()); } } public class Areagroup : IModel { public int ID { get; set; } public string Name { get; set; } public string DisplayName { get; set; } public int SortKey { get; set; } public int ColorR { get; set; } public int ColorG { get; set; } public int ColorB { get; set; } public int ForeColorBW { get; set; } public int ExcelColor { get; set; } public int Enabled { get; set; } public DateTime modified { get; set; } public DateTime created { get; set; } public Areagroup(XElement el) { base.FormXml(el); } } public class Sellingcompany : IModel { public int ID { get; set; } public int ManufacturerID { get; set; } public int JurisdictionID { get; set; } public string Name { get; set; } public string Abbreviation { get; set; } public string Code { get; set; } public int Enabled { get; set; } public DateTime modified { get; set; } public DateTime created { get; set; } public Sellingcompany(XElement el) { base.FormXml(el); } }
それぞれのプロパティはちまちまと書いてもいいのですが、Visual Studio 2013 だと「編集」メニューの「型式を選択して貼り付け」→「XMLをクラスとして貼り付ける」を選ぶと簡単に作れます…が、JSONのときとXMLの時と違う出力になるので、なんだかな~という感じなのと、MVVM の Model にするときはちまちまと手作業で変えないといけません。まあ、その時は適当なスクリプトを書けば済むわけですが。
Store クラスのコンストラクタがややこしいことになっているのは、Web APIが返すXML構造とModel.Storeクラスのツリー構造が異なるからです。ここを同じにしてもよいのですが、CakePHPの出力とWPFでバインドをするときの便利さとが異なるの、ここで調節しておくほうがベターです。
■リストビューを作る
左側のリストビューを作ります。DataContextを使ったデータバインドが前提になるので、ちまちまとバインド式を書いてきます。
最低限バインドしておく箇所は、
- ListView に ItemsSource=”{Binding Items}” でバインド
- それぞれの列に DisplayMemberBinding=”{Binding ID}” でバインド
ビューにバインドするモデルから Items プロパティで bind させます。そして、それぞれのアイテム(要素)のプロパティにバインドさせるわけですが、DisplayMemberBinding=”{Binding Sellingcompany.Name}” のように、内部もつクラスのプロパティを参照することができます。ツリー構造にしておくと、親オブジェクトをリスト化しておいて、それぞれのプロパティにアクセスができます。
<ListView x:Name="lv" ItemsSource="{Binding Items}" SelectionChanged="lv_SelectionChanged" HorizontalAlignment="Left" Height="235" Margin="10,10,0,0" Grid.Row="1" VerticalAlignment="Top" Width="315"> <ListView.View> <GridView> <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" Width="50"/> <GridViewColumn Header="名前" DisplayMemberBinding="{Binding Name}" Width="50"/> <GridViewColumn Header="会社名" DisplayMemberBinding="{Binding Sellingcompany.Name}" Width="50"/> <GridViewColumn Header="地域" DisplayMemberBinding="{Binding Areagroup.Name}" Width="50"/> </GridView> </ListView.View> </ListView>
プロパティが入れ子になってバインドができるのは便利なのですが、逆に言えばプロパティ名が変わったときには、XAML 部分も直さないといけないという弊害もあります。そのあたりは先の Web API とデータモデルの自動変換のところにも当てはめるわけで、それはそれで割り切りで。業務のほうは、SellingcompanyName という読み取り専用のプロパティを作ったりしてます。
■プロパティビューっぽいものを作る
リストビューをクリックしたときの内容を表示させます。
リストビューと似た形で Item という名前で DataContext へバインドしておきます。この「Item」の部分は、自分で好きなように設定できます。
<TextBox Text="{Binding Item.ID}" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="10,13,0,0" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" Width="162"/> <TextBox Text="{Binding Item.Name}" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="10,41,0,0" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" Width="162"/> <TextBox Text="{Binding Item.Sellingcompany.Name}" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="10,69,0,0" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" Width="162"/> <TextBox Text="{Binding Item.Areagroup.Name}" Grid.Column="1" HorizontalAlignment="Left" Height="23" Margin="10,97,0,0" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" Width="162"/>
Text=”{Binding Item.ID}” のところがバインドですね。地域の名前を表示するときは Text=”{Binding Item.Areagroup.Name}” な感じでバインドさせます。
TextBox にバインドさせているのは、編集もできるようにするためです。ただし、不用意に編集されないように読み取り専用のときと編集可能なときのモードをつけるほうがベターです。これは、IsEnabled に対してバインドさせると一気に Textbox を読み取り専用にすることができます。
■バインド用のモデルを作る
WPFのビューにバインドさせるモデルクラスを作ります。INotifyPropertyChanged インターフェースを使うわけですが、Visual Studio 2012 に入っているストアアプリ用の BindableBase.cs をそのまま使っています。このあたりは適宜好み&用途にあわせて。
public class BindModel : BindableBase { private ObservableCollection<Model.Store> _Items; public ObservableCollection<Model.Store> Items { get { return _Items; } set { this.SetProperty(ref this._Items, value); } } private Model.Store _Item; public Model.Store Item { get { return _Item; } set { this.SetProperty(ref this._Item, value); } } }
リストのほうは、ObservableCollection を使うのを忘れずに。
本来ならば Model.Store クラスの各プロパティも INotifyPropertyChanged を通すべきなのですが、単なる閲覧の場合は不要なのでこのままで。
Items コレクションがリストビューにバインドするプロパティで、Item プロパティがプロパティウィンドウのほうにバインドするものです。
■DataContextに設定する
ビューの DataContext にモデルを結びつけます。XAMLにStaticResourceで追加してもよいのですが、好みコードで設定しています。
public MainWindow() { InitializeComponent(); // モデルをバインドする _model = new BindModel(); this.DataContext = _model; } private BindModel _model; /// <summary> /// 一覧を取得 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private async void Button_Click(object sender, RoutedEventArgs e) { var lst = await new Api.ApiStore().Read(); _model.Items = new ObservableCollection<Model.Store>(lst); // this.lv.ItemsSource = lst; } /// <summary> /// 項目を選択 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void lv_SelectionChanged(object sender, SelectionChangedEventArgs e) { var item = lv.SelectedItem as Model.Store; _model.Item = item; }
リストビューの更新は、_model.Items 経由で、リストビューの行を選択したときは _model.Item 経由で表示させます。
CakePHP で作った Web API の呼び出しが new Api.ApiStore() となっているのは愛嬌です。本来ならば static メソッドにするか、別途 api 用のオブジェクトをキープしておくかという感じなんですが、まあ、new 使ってガシガシと開くところは Java っぽいですね。この new オブジェクトはいつ解放されるのだろうか?ってなことは C++ の時だけ考えます。
■Web APIを呼び出す
Web API の呼び出し部分も定型なので、何かと共通化しておきたいところですが、いまんところべた書きです。どうしても Store のようなクラス名が出てきてしまうので、あまり汎用的には作れません。T4 使って自動生成するぐらいかなと。エラー処理がおおざっぱなのは、所詮マスター画面で使うものだからです。基本はエラーがでないという思想ですね。
public async Task<List<Model.Store>> Read() { var uri = new Uri(SERVER + "/Stores.xml"); var lst = new List<Model.Store>(); try { HttpClient cl = new HttpClient(); var xml = await cl.GetStringAsync(uri); var doc = XDocument.Parse(xml); var coll = doc.Descendants("item"); foreach (var it in coll) { var el = it.Descendants("Store").FirstOrDefault(); var m = new Model.Store(el); lst.Add(m); } } catch ( Exception ex ) { // エラー発生 Debug.WriteLine("error:{0}", ex.Message); return lst; } return lst; } public async Task<Model.Store> Read( int id ) { var uri = new Uri(SERVER + string.Format("/Stores/view/{0}.xml", id)); try { HttpClient cl = new HttpClient(); var xml = await cl.GetStringAsync(uri); var doc = XDocument.Parse(xml); var el = doc.Descendants("Store").FirstOrDefault(); var m = new Model.Store(el); return m; } catch (Exception ex) { // エラー発生 Debug.WriteLine("error:{0}", ex.Message); return null; } }
Web APIのエラー値を取るためには、もう少しコードを追加しないといけません。
ひとまず、こんな感じで WPF から CakePHP の Web API を呼び出すことができます。