続・受託開発の罠

受託開発の罠
http://www.marenijr.net/mymy/solution/008.html

を書いたのは実は5年前です。読み直すと誤字が多いのと、意味不明な形容詞が難ですが。
で、なんとなく検索して、リンクを探してたら、

網助っ人 芥川武日記
http://blog.netsket.com/acty/2007/04/post_9.php
でリンクされていました。ここの芥川さん Netsket という会社の社長さんです。
http://www.netsket.com/

この会社の最近の売り(?)は「マッハ開発」だそうです。
詳しくは会社のページを見てほしいのですが、所謂、WEBサービスを1週間でリリースしますという商売/スタイルです。普通の開発では考えられない(こともない?)わけですが、これがこの会社の強み&差別化です。

強み/差別化の話は起業塾でもとくテーマになります。ソフトウェア業界の場合、製品開発よりも受託開発(システム一品もの)が圧倒的に多いので、どこの会社に発注するのか、どこの会社/人と継続的に繋がりを持つのか(営業の削減、取引コストの削減)が、請け負う会社/人にとっては生命線になります。最悪(でもないけど)会社の場合は営業収入的にじり貧になって畳む、ということになります(当然、最悪の場合は借金ね)。

そう、マッハ開発っぽいことの発想は、WEB開発は当然で、案を出されてから2週間でリリースというのが理想的だそうです。平均的なWEB開発が2か月以内だそうですから、相当の早期リリースを求められているわけですね。

「素早く対応しないといけない日進月歩の業界ですね」

な~んてことは私は言いません。
反感を買うであろうことを敢えて言えば(書いておけば)、

「先見の目がない学習しない業界ですね」

ってことです。所謂、単純な短納期はイコール無計画ゆえの短納期を自己言及しています。
これを流れで示せば、

1.あ~、最近うちの会社のページがパッとしないんだよね。何か集客はあるかな?
2.そういえば、○○ってのが流行っているみたいですね。
3.そうそう、○○ってのは、あの会社もやり始めたしネットでも話題だし。
4.なるほど、じゃあ○○を活用してうちの会社の××と組み合わせれば。
5.それ、いいじゃないですか!
6.でも、見積もりとか期間とかあれだから、そうするとブームが去っちゃいますよね。
7.いや、大丈夫、あそこの会社のスピード開発に頼めば!
8.そうか、じゃあ、お願いします!

ってな具合なのかな~、と妄想していします。そう妄想ですからね、妄想!

というわけで、極端な短納期は差別化の一種ではあるのですが、懸念するのは(いえ、別にネット助っ人という会社を批判している訳ではなく、一般的な市場を話をしています)、そういうコンビニみたいな100円均一みたいな作り方/会社/人生がしたいのか?ってことです。

5年前、受託開発の罠を書いたときに「ソフトウェア業界=ゼネコン」と気づいた時から、建設業界の比較を考えてきました(ずっとではないけど)。5年前から比べると、株価は暴落したし、不況は更にひどくなっているし、ソフトウェア開発者の単価は下がりっぱなしだし、当時そこまでは予想していなかったけど「罠」に嵌ると抜け出せない会社が多々ありそうな雰囲気です。

先の話を書いたときには、顧客>SIer>下請のヒエラルキーから抜け出す方法は、このシステムから抜け出す方法以外にない、とは分かっていました。ですが、具体的に何をすれば抜け出せるのかはよく分かりませんでした。

という訳で、前置きが長くなりましたが、久々の続編ということで、具体的な解決策をいくつか書いておきます。

■製品を作る

第一に思いつくことが、顧客に直結するために製品を作ることです。これは「顧客>SIer>下請」の SIer の項を抜き去り「顧客>自社」とするシステムを確立します(SIerになるのではない!ので注意)。

他業種で言えば、小売業、タクシー、ホテルなどのサービス業種です。顧客(お金を払う人)と直結して取引を行い自社のアイデア/製品を提供し続けます。
小売を目指すのであれば、シェアウェアの提供、iPhoneなどのアプリが思いつきますが、会社として経営するのは難しい面があります(秀丸は例外でしょう)。現状、小売業のように一般人を相手する場合、MS-Officeやウィルス対策ソフトのようなパッケージが一般的です。これらのパッケージソフトは大手の会社が配布している場合が多いので、小さな会社は手がだし辛いでしょう。だいたいパッケージソフト=製品とは言え、自社のみで作ることは少なく、先のSIerが製品のアイデア/提案をし、下請けが作るという仕組みが確立されているので、自社がSIerになるしかない、というジレンマがあります。

ですので、ホテルのようなサービス業種を真似し、顧客を特定の分野に絞っていきます。たとえば、商店街の店であったり、小規模の工場であったり、小規模の官庁であったりします。
この場合、それぞれの顧客について受けいられる予算は小さいものですから、受託開発で行うような一品ものの製品を作っては割が合いません。ひとつの製品を複数の顧客(多数ではないが1つではない)に売るなり、ひとつの製品を自社で手軽に/手早くカスタマイズして顧客に提供する、という方針になります。

手軽に/手早くというのが、会社の暗黙知/資産になります。ノウハウとも言いますね。どちらかと言えば、このノウハウは人あるいはチームに属すると良い面があります。

このあたり、具体例を出せば、

・自社のフレームワークを使うと、開発が効率化できる。
・自社のフレームワークを使うと、超短納期でできる。
・コア技術を持ち、それに派生した製品を作成する。
・コア技術を持ち、それと顧客を組み合わせた製品を作成する。
・自社のチームを使うと、一定分野に特化した開発ができる。

一見、受託開発でも成り立つ論理に見えますが、営業を行わない(こともないけど)受託開発の場合には開発の効率化/短納期がイコール受注額の減少になり、逆効果になります。
当たり前ですが、中間に位置するSIerの取り分を多くするためには、2つの方法がありますが、下請けへの発注額を減らすほうが楽なのです。幸いにして(?)受託専門の会社はたくさんありますので、この不況下金額を下げる価格競争が「競争」になってしまい、下請け/自社の取り分は減ってしまい、単なる生き残り合戦になってしまいます。
しかし、顧客と直結した場合、受注額を減らしたとしても、それ以上の効率化を行う(あるいは売り手として受注額を計算する)ことにより、経験/学習/努力の成果は時間が余るという効果を出し、先行きへの学習/製品作りの時間を取ることが可能になります。
そういう会社/人の成長過程を踏むためにも、顧客>自社の直結が必要であり、なんらかの「製品」を持つことがソフトウェア業界でも必須になります。

■取引コストを下げる

取引コストというのは「何故コンビニで98円のジュースを買わず、コンビニの前にある自動販売機で120円のジュースを買うのか」という話になります。
人が何かモノを買うときには、無意識にいくつかのコストを試算しています。

・ひとつはモノの価格としてのコスト
・ひとつは差別化/付加価値としてのコスト
・ひとつは物珍しさ/驚きとしてのコスト
・ひとつは買うときに交渉する取引コスト

などです。正確には5つあるはずなのですが、ここでは4番目の交渉/取引コストを問題にします。取引コストは企業では営業職の人です。端的に言えば、モノを好きな時に定価で買う、好きな時に定価で売る、ことができれば営業という職種はいりません。残念ながら(?)現在の社会では需要と供給がイコールになることはまれで、そこには競争が発生します。ひとつは価格競争なのですが、もうひとつはコミュニケーションとしての競争があります。これが「営業」です。
さて、営業の厳密な定義はさておき、この営業コスト=取引コストは、価格を交渉したり、レジにジュースを持って行ったり、相手の予定を窺ったり/伺ったり、顔つなぎをしたり、という役目に発生します。だから、コンビニに入って冷蔵庫からジュースを取り出してレジに持っていくのが「うっとおしい」と感じたとき、人はコンビニの前の自動販売機でジュースを買います。
このあたり、生物経済学(だっけ?)に詳しく載っています。

というわけで、人はモノを買うときコストに関しては一見理不尽な行動を取ります。
先の「うっとおしい」という感情は、社会的に問題を抱えつつも、人数が多すぎる日本社会の一面を表しています。

これは、会社間の交渉にも言えます。
常に営業を介して、価格交渉をし続けるよりも、ある程度の幅を持った金額のやり取りを続けるだけで十分な時があります。これが、会社間の継続的な関係です。
継続的な関係は、顧客と自社だけでなく、自社とグループ会社の間でも発生します。昨今のグループ会社を作る動きは、外側への営業を一本化して、自社のあふれた分をグループ会社に廻す、同様にグループ会社のあふれた分を自社に廻して貰うという関係が前提になっています。ここでも「営業」特有の取引を減らす努力をなされています(営業機会を増やす努力でもありますが)。

この会社間の関係を長く続けると、グループ内での営業金額のやり取りは差し引きゼロになります。つまり、トータルで見て営業的なやり取りが不要ということです。つまり、取引コストがゼロになるということです。
こうなると不思議なことに、グループ内でのやり取りはあたかも定価を通じて行っているように見えます。つまり、好きなときに定価で売る、好きなときに定価で買うという状態です。

そう、ヤクルトを社内に置いたり、薬箱を家庭に置いたりするような状態と同じものが作れます。

Amazon を始めとするインターネットでの小売の増加は、当初この取引コストを減らす動きでした。しかし、インターネット自体が普及するに従って(あるいは Amazon が販路を拡大する路線に従って)、インターネットでの製品も価格コストが下げる傾向にあります。
しかし、本来ならば家で買える便利さ/品物の豊富さを見れば、さほど価格を下げる必要はないと思われます。これは(顧客にとって)不要なものを売るという、販売過剰の状態になっていることを示しています。

■システム/サイクルを新しく作る

さて、先の「顧客>SIer>下請け」を脱却するためには、このシステムから脱却することが一番です、と言いました。「顧客>自社」の体制にしてしまえばいい訳で、そうなると別のシステム/サイクルを作るのが一番です。

注意しておきますが、ここから話すことは話し半分に聞いてください。試行中ですから。

株式会社の場合、会社法的に「株主」が会社を所有します。ですから、会社の所有物/付属物である従業員は株主に所有されているわけです。となれば、従業員が会社を支えるときに、配当を利益の一部から出し、他は役員報酬として分配し、従業員が最低限働くだけの賃金を渡せば、それが会社は成り立ちます。法的には。
この負のスパイラルが多くの現不況下で続いている訳で、従業員である限り、この見えない「株主」という存在から抜け出せません。あるいは、会社への金銭的な投資という束縛から抜けられません。
当然、日本の場合、家族経営型の会社が多かったので、この株主の存在だけでなく、チーム/グループとしての従業員を考えた理念を掲げることが多いのですが、会社自体にこの理念を貫く理由が失われつつあります。
となれば、会社法に従う、株主/役員のための会社が社会生物学的に残ります。

こうなると、下請けであっても役員であれば、従業員よりも多くの報酬を得ます。当然「SIer>下請け」の関係同様に「役員>従業員」の関係が成り立ちます。すると、搾取とまではいいませんが実際、モノを作るのは従業員でありつつ、賃金は安くというスパイラルに入ります。

これを脱する方法があるのか?と言われれば、私は「ない」ような気がします。(あまり信頼できませんが)金持ち父さんシリーズのロバート・キヨサキもそう言います(こちらは別途問題ありですが)。
生きる場を変えるということで「E」の場所に身を置くように努力するのもひとつの方法ですが、残念ながら彼の云うマトリクスはゼロサムゲームでしかありません。

なので、この場合は「株主」を廃してしまうのが適当と思われます。
一時期盛んに行われた株式の上場取りやめ、非上場化がその動きです。最近では株価の安定のためか(?)あまり記事になりませんが。
本来は資金を得るために株式を上場するはずだったのが、株式上場を果たして役員が報酬を得る(手持ちの株価を上げる)という形にスライドしています。つまり、本来は広く資金を集めるために株式を上場し、その資金を元に研究開発/雇用者の増大を図ったわけですが、最近は上場した株の多くを役員が持ちストックオプションとして売り、家や車を買うという「報酬」になっています。ここが間違いなのです。

こうなると、システムを変える(あるいはシステムを戻す)方法として対策が思い浮かびます。

・手持ちの資金で製品開発ができる範囲に絞る。
・非公開株で資金を集め、優秀な従業員を手元に集める(チームを作る)。
・取引コストの低い相手のみ、取引対象とする。
・過剰な資金を求めず、欲しい資金を明確にし、資金源を得る。
ここまで書きましたが、まだ整理がついていません。
続きや整理は久しぶりに MyMy-MyCompany でしようと思います。
http://www.marenijr.net/mymy/

カテゴリー: 起業塾 | 続・受託開発の罠 はコメントを受け付けていません

SQL Server Management Studioでアクセス権情報を作る

泣いていてもしかたがないので、SQL Serverのアクセス権をクエリで設定する方法を調べてみると、

grant select on [テーブル名] to [ロール]

な感じで GRANT を使います。ちなみに、削除をすると場合は、↓な感じ。

revoke select on [テーブル名] to [ロール]

# 実は至高の技のワンポイントに書いていたよ…自分で忘れてた。
さて、手作業で設定はできることは分かった。が、GUIで設定した後にアクセス権の構成情報を取得する手段がない。

そう、気付いたのだが、何も SQL Server Management Studio だけが出力だけが出力できないのではなくて、Oracle や MySQL の場合も出力できない。というわけで、何も SQL Server Management Studio だけが悪いってわけではないんだね。う~む。

業務で DBA の真似ごとをしていて必要なのは、順序付ければ以下のようになります。

a) テーブル、インデックスの構成情報
b) データベースの再構成
c) ログインユーザーの構成情報
d) ストアドプロシージャの構成情報
e) アクセス権の構成情報

このあたり、SQL Server の場合は手厚くて、a)からd)まで出してくれる。だけども、Oracleの場合、a)もひと苦労で、b),c)に至っては初期の情報がないと手に入れるのが難しく、d)はスクリプトがあるんだが、という状態。
まぁ、ソフトウェア開発者的には必要ないんだが、SE(システムエンジニア)イコール大手の社員(プロパー)を示すようになってしまった現在においては、DBAなり設計者なりの能力は低いと判断して構わない。インフラ整備をしている個人事業主のSEの実力はよくわからないのだけど。
少なくとも、インフラ関係の情報は google などでは取得しにくいのは確か。

閑話休題。

実はアクセス権の設定は優先順位が低い。というのも、大抵のプロジェクトはa)からc)で力尽きて(?)しまうわけで、ストアドプロシージャとかアクセス権まで手が廻らないからです。
ただし、ストアドプロシージャに関しては、

・適切な分業体制を作る。
・WEBインフラ、C/Sシステムにて適切な実行環境を得る。

ってな訳で相当重要なことなのだが、忘れ去られつつある(ように見える)。

アクセス権に関しては更に重要で、

・情報の保護
・バックアップの重要度により分離

なところがあるにも関わらず、かなりいい加減なログインユーザーの設定をしていたりする。あいにく、銀行系のデータベースを触ったことがないので、そうなのかもしれないが、情報系のデータベースはかなりいい加減だ(社内で使うからどうでもいいという話もあるが)。

というわけで、(もし)アクセス権を設定しているならば、sp_helprotect を使うと取得できる。

ネタ元
http://q.hatena.ne.jp/1122874335

sp_helprotect @username='limit'

何も付けないとすべてのアクセス権を拾ってしまうので、@username にログイン名/ロール名を指定する。この場合は limit ロールのアクセス権を拾うわけだ。

結果をみると↓のようになる。

Owner Object Grantee Grantor ProtectType Action Column
dbo t_person limit dbo Grant      Select (All+New)
dbo t_在庫 limit dbo Grant      Select (All+New)
dbo t_商品 limit dbo Grant      Select (All+New)

主に見ないといけないのは、ProtectType列とAction列らしい。
ここでは設定として、limitロールに次の3つのテーブルに対して参照権限(SELECT)を与えている。
・t_person
・t_在庫
・t_商品
逆に言えば、ほかのテーブルを見ることができない。
与えるほうはProtectType列が「Grant」、拒否が「Revoke」になる。

# ちなみに以下のマニュアルを見ると、「GRANT」「REVOKE」と大文字になっている。
# SQL Serverの初期値では大文字小文字を区別しないから、どちらでもいいのだが、
# 設定次第だと区別することになるので、この記述は問題だなぁ。

Transact-SQL リファレンス
sp_helprotect
http://msdn.microsoft.com/ja-jp/library/aa933420(SQL.80).aspx

 

実データは、sysprotects テーブルらしいので、こっちから取得するのもあり。

Transact-SQL リファレンス
sysprotects
http://msdn.microsoft.com/ja-jp/library/aa260449(SQL.80).aspx

というわけで、元ネタが分かったので簡単なクエリを書いてみよう。

begin
-- 対象のロールを設定
 declare @name sysname = 'limit'
-- 一時テーブルを作成
 create table #temp (
  [owner] sysname,
  [object] sysname,
  [grantee] sysname,
  [grantor] sysname,
  [protecttype] char(10),
  [action] varchar(20),
  [column] sysname
  )
 insert into #temp execute sp_helprotect @username=@name
-- スクリプトを作成
 select
  [protecttype] + ' ' + [action]
  + ' on [' + [object] + ']' +
  + ' to [' + [grantee] + ']' as query
 from #temp
-- 一時テーブルを削除
 drop table #temp
end

sp_helprotect ストアドプロシージャの結果を直接 FROM 句では受けられないので、一時テーブルを使う。この一時テーブルに対して、文字列を加工して GRANT のスクリプトを生成すればOK。括弧で括ってあるのは念のため。スクリプトを加工する場合は置換で面倒なので外したほうがベターでしょう(括弧は正規表現の予約語だからね)。

結果はこんな感じ

Grant      Select on [t_person] to [limit]
Grant      Select on [t_在庫] to [limit]
Grant      Select on [t_商品] to [limit]

# 一時テーブルのアイデアは下記より借用

なにやらかにやらメモ SQLServer
http://www31.atwiki.jp/memo77/pages/22.html
中の技術日誌ブログ – ストアドプロシージャって難しいなぁ
http://blogs.wankuma.com/naka/archive/2004/03/07/1607.aspx

メンテ専用のストアドプロシージャを作るのもよいのだが、まあ、こんな風にブログにぺたと貼り付けておいて、コピー&ペーストで実行するのもありかな、と。

カテゴリー: 開発 | SQL Server Management Studioでアクセス権情報を作る はコメントを受け付けていません

SQL Server Management Studioでデータベース構成情報が作れない

SQL Server 2005/2008を使っているときは、SQL Server Management Studio を使うと便利だ。

Microsoft SQL Server Management Studio Express
http://www.microsoft.com/downloads/details.aspx?displaylang=ja&FamilyID=c243a5ae-4bd1-4e3d-94b8-5a0f62bf7796

使っているけど(多分)Express版でも製品版でも同じ。GUIでぽちぽちするだけで構成がわかるので便利なんだけど。落とし穴がある(のかな?)。

特定のテーブルへのアクセス制限をするときに、

1.ロールを作る。
2.ロールへログインユーザを加える。
3.ログインユーザからdb_ownerを外す。
4.ロールにテーブルアクセス権を与える。

ということをすると思うんだが。
# 最近はアクセス権自体を触らないDBA(とは言わないな)もいるので、
# なんとも言えませんが。

SQL Server 6.0の頃はEnterprise Managerを使って、スクリプトを吐き出していた。そして手で少し修正するぐらい。
で、同じようにのManagement StudioのGUIでロールを作成、アクセス権の設定をした後に、スクリプトを吐き出してみたら、、、あらら、先の2,3,4のスクリプトが全く吐き出されていないじゃないですか!

具体的に書くと、次のように limit ロールと、normal ユーザがスクリプトで作成できているのだが、

CREATE ROLE [limit] AUTHORIZATION [dbo]
GO
CREATE LOGIN [normal] WITH PASSWORD=N'normal', DEFAULT_DATABASE=[sampledb], 
 CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
GO

以下のように db_owner から取り除いたり、limit ロールに加えたりするコマンドが何処にもありません。

exec sp_droprolemember 'db_owner', 'normal'
go
exec sp_addrolemember 'limit', 'normal'
go

更に言えば、アクセス権の設定も全くでていません。
ん~、どうなんでしょうね。sp_droprolemember, sp_addrolemember のシステム系のストアドをヘルプから探さないとデータベースの構成情報が作れない、というのどんなもんなんでしょう?

というわけで、これから手作業でデータベース構築のスクリプトを書かなければならず。しくしく。

カテゴリー: 開発 | SQL Server Management Studioでデータベース構成情報が作れない はコメントを受け付けていません

咲-saki-とドカベン

完全な雑談です。

「咲-saki-」という漫画/アニメをご存じでしょうか?高校生が麻雀大会で全国決勝を目指すっていう話なのですが、「けいおん!」に近いというか、大会のナレータ役が「らき☆すた」のあれだったりと、なんか違和感がないので。DVDで見ていたりするのですが。いや、違和感がないのほうが普通じゃないですが(苦笑

さて、現時点で漫画のほうは読んではいませんが、アニメのほうはいわゆる萌え系の絵でストーリーも多少そんなところもあります。しかし、ん~、この高校大会って感触は、ひょっとすると、、、「ドカベン」に似ているのでは!?と思った人は、私だけではないハズハズハズ……私だけですかねぇ。

麻雀漫画ですから、役なり流れなり論理思考っぽいとろが出てきます。でも主人公の咲が嶺上開花で上がり捲るあたり、あの哭きの竜の彷彿とさせますが、あちらのほうは一人が戦う個人戦(つーか、やくざの世界だけど)。でも、咲の世界は高校選手権=団体戦っていうあたり、そして、嶺上開花という特技(?)を持っているあたり、サヨナラホームランを武器(?)とする山田太郎にそっくりです!

そして!、チーム戦ですから、相手の高校にもすごい技を持っている人いるんですよね。ステルスモードとか海底摸月狙いだったり、地獄待ちだったり、ネット麻雀と得意とする論理思考だったり、と(見た人は分かるよね)。
このあたりも、坂田三吉とか不知火とか岩鬼とか殿馬とか、野球のルールに則った得意技を持つキャラがいますよね(見た人は分かるよね!)。

そして、更におもしろいのはそれぞれのキャラに何らかの設定(過去)があって、それを踏まえて今の得意技があるってところです。ここらあたりもドカベンと似ていて、厚みっというかその取って付けた過去に好感が持てます。というか、水島新司の創始がそれだけすごいわけですが、この咲の原作者/脚本家/スタッフも偉いですよね。
チームで県予選から全国を目指すってところ、スラムダンクにも似ています。アタックナンバーワンもそうですね。あいにくスラムダンクには詳しくないので、比較している人/サイトがいると有り難いかな(本当にありがたいのか?)。

と思ったら、いました、いましたスラムダンク風に解説しています。

http://d.hatena.ne.jp/tatsu2/20090908/p1
http://ameblo.jp/yorunoyogiri/entry-10327255685.html

ぜひ、ドカベン風にも語って欲しいものです。

カテゴリー: 雑談 | 5件のコメント

Silverlight の UnitTest

これまでのテストをするためにUnitTestを使っていますが、所謂↓を使っています。

Silverlight Unit Test Framework
http://code.msdn.microsoft.com/silverlightut/

ロジックだけであれば、NUnitとかMicrosoftが提供するTest Frameworkを使えばいいのですが、画面が絡むと途端に面倒になるので。

さて、この「Silverlight Unit Test Framework」ですが、ダミーでボタンをぽちぽち押してくれるようなタイプではありません。先のエントリを見ると分ると思いますが、他のUnitTestと同様にメソッドを羅列していきます。
なので、画面のフォーカス/アウトフォーカスみたいなイベントを取れません。
まあ、本当はそれで動かないと駄目なんですけど、試してみると画面の動きが違ったりして(特に間違ったDataContextの指定の仕方とかすると)なにかとややこしいのですが。
このあたりのイベント絡みをやるためには、別途javascriptとかを組まないと駄目そうですね。

画面のほうは、至ってシンプルです。
ただし、テスト結果がブラウザ上でしか確認できないので、自動化には向いていないそうです(というブログがありました)。

さて、使い始めは何事も躓きやすいのでポイントだけ示しておきます。

■プロジェクトを3分割する

UnitTestをする場合、プロジェクトを3つに分けます。

・テスト対象のプロジェクト(SampleTextBinding)
・テストプロジェクト(SampleTextBinding.Test)
・テスト実行のプロジェクト(SampleTextBinding.Test.Web)

画面やロジックしてあるのは SampleTextBinding のプロジェクトです。
出来る限りここには手を加えないようにします。

テストプロジェクトは TestCase を書いていきます。

テスト実行のプロジェクトはWebのプロジェクトです。テストプロジェクトと一緒にしてもいい気もしますが、なんかうまくいかないので、別々にしました。

■テストプロジェクトに3つの参照を追加する

最初にテストプロジェクトに手を入れます。
参照設定に以下のアセンブリを追加します。

・Microsoft.Silverlight.Testing
・Microsoft.Silverlight.Testing.Framework
・Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight

■テストケースを作る

テストケースのひな形は、マイテンプレートから「Silverlight Test Case」で追加してもよいですし、
次のひな形を使っても構いません。

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SampleTextBinding;
using Microsoft.Silverlight.Testing;
using System.ComponentModel;

namespace SampleTextBinding.Test
{
  [TestClass]
  public class TestCase001 : SilverlightTest
  {
    private Page _page;
  
    [TestInitialize]
    public void SetUp()
    {
      _page = new Page();
    }

    [TestMethod]
    public void TestMethod001()
    {
      // ここに記述
    }
  }
}

ひとまとまりのテストケースには「TestClass」の属性を付けます。
SilverlightTest クラスは普通は継承しなくてもよいのですが、非同期のテストの場合はこれを継承します。

私の場合は、必ずページを開くことになるので、初期化時のメソッドSetUpに「TestInitialize」の属性をつけて、初回に実行するようにします。

メソッドを書くときは「TestMethod」をつければOK。

最初は練習用に

public void TestMethod001()
{
  Assert.AreEqual( 1,1 );
}

のように必ず通るテストを付けておけばOKです。

そうそう、忘れていけないのは、App.xml.cs の修正です。

private void Application_Startup(object sender, StartupEventArgs e)
{
  //  this.RootVisual = new Page();
  this.RootVisual = (UIElement)UnitTestSystem.CreateTestPage(this);
}

こんな風にテストページを RootVisual に設定してください。
このとき Silverlight 2 の場合は UIElement へのキャストが必要です。

■テストを動作させるプロジェクトを設定する

SampleTextBinding.Test.Web プロジェクトのことですね。このプロジェクトにテストを動作させるページを追加します。
まず、プロジェクトを右クリックして「プロパティ」を選択します。
「Silverlightアプリケーション」のタブを開いて、追加ボタンを押します。
対象のアプリケーションを選択できるので「SampleTextBinding.Test」のほうを選択します。

すると、SampleTextBinding.TestTestPage.html のようなページができるのでスタートアップページに設定します。そうそう、SampleTextBinding.Test.Web プロジェクトをスタートアップに設定してください。

さて、これで準備完了。どんどんテストを行ってください。

画面のプロジェクトを「なるべく触らない」と書きましたが、実はボタンイベントの場合は触らないと駄目です。
実はボタンイベントは private で設定されてます。

private void BtnSave_Click(object sender, RoutedEventArgs e)
{
  ...
}

なので、これを外側から呼び出すことはできません。public に変えてもいいのですが、本番のクラスのアクセス権を変更してしまうのは問題があります。

というわけで仕方がないので internal を使ったメソッドを作ります。

/// 以下、テスト用
internal void TestBtnSaveClick()
{
  BtnSave_Click(this, new RoutedEventArgs());
}

internal は同じアセンブリから呼べる、という protected と public の間みたいな制限を付けられます。
ここで裏ワザ(かな?)があり、AssemblyInfo.cs に次の記述を書くと別アセンブリのクラスから呼び出せます。

[assembly: InternalsVisibleTo("SampleTextBinding.Test")]

こうすることで、TestMethod のほうから、無事 TestBtnSaveClick を呼べるようになり、ボタンクリックのイベントを発生させられるのです。
Silverlight Unit Test Framework のページには、この他に非同期呼び出しのテストの解説も載っているので参考にしてください。

余談

ボタンのイベントなんですが、Java の UnitTest の潮流から言えば、この internal の方法が正しいのですが、テスト用とはいえ、internal メソッドを入れてしまうのが気に入りません。
できることなら、Windows アプリの SendMessage や SendKey みたいなのでエミュレートしたいですねぇ。

カテゴリー: 開発 | Silverlight の UnitTest はコメントを受け付けていません

XAMLの動的バインド

さて、Silverlight+MVVMの話の続きです。
単純に xaml のコントロール名を使えば楽なのですが、MVVM に則るといちいち{Binding なんとか}を書かないといけません。Binding の構文は、定型といえば定型なのですが、View の中に乱立するのがいまいち解せません。

解せない理由はと言うと、

・そもそも View と Model を分離して View はデザイナ、Model はプログラマ、と切り分けができるのではなかったか?
・View 自体は Microsoft Exprssion Blend を使って編集するわけだから、いちいち Binding の分が書いてあるのはどうかと思う。
・間違って消しちゃったら、Binding は直すの大変。ここのコードを書くのはプログラマなんだから、Viewに手を加えるの問題あり。

というわけで、xaml のほうに Binding を書くのは、かなり解せないのです。
勿論、xaml に書かずに済ますには?というか、方法を模索しないといけないわけで、そうなると対応する *.xaml.cs に書くのがいいですよね。

<Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
    <StackPanel>
        <Button Name=&quot;BtnSave&quot; Width=&quot;100&quot; Content=&quot;保存&quot; Click=&quot;BtnSave_Click&quot; />
        <TextBox Name=&quot;TextName&quot; Width=&quot;100&quot; Text=&quot;{Binding LastName, Mode=TwoWay}&quot; />
        <TextBox Name=&quot;TextOut&quot; Width=&quot;100&quot; Text=&quot;{Binding OutName, Mode=TwoWay}&quot; />
    </StackPanel>
</Grid>

名前は仕方がないとして、Text=”{Binding LastName, Mode=TwoWay}” はデザインから見ると異様です。第一文字列が長いし、何を書いているのかさっぱりわかりません。xaml を直接編集しようにも Binding の部分が長くて編集しづらいです。

なので、できるならば↓な風にしたい。

<Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
    <StackPanel>
        <Button Name=&quot;BtnSave&quot; Width=&quot;100&quot; Content=&quot;保存&quot;/>
        <TextBox Name=&quot;TextName&quot; Width=&quot;100&quot; />
        <TextBox Name=&quot;TextOut&quot; Width=&quot;100&quot; />
    </StackPanel>
</Grid>

ほら、すっきりしたじゃないですか。これならば、WidthプロパティとかContentプロパティとかを編集しやすいですよね。Expression Blend を使っても名前(Name)を設定できますが、バインドのほうは無理です。となれば、デザイナが想像するのはこれくらいシンプルなxamlなはずです。
# まぁ、ストーリーボードとかフィルタが入るとごちゃごちゃしますが。

というわけで、バインドはどうするのかと言うと *.xaml.cs のほうで書きます。コードビハインドを利用するわけですね。

まずはコードを見てください。

public Page2()
{
    InitializeComponent();

    // 動的にバインディング
    Binding bi;
    bi = new Binding("LastName"); 
    bi.Mode = BindingMode.TwoWay;
    this.TextName.SetBinding(TextBox.TextProperty, bi);
    bi = new Binding("OutName"); 
    bi.Mode = BindingMode.TwoWay;
    this.TextOut.SetBinding(TextBox.TextProperty, bi);
    
    // コマンドも動的に設定
    this.BtnSave.Click += BtnSave_Click;

    // データコンテキストに設定
    this.DataContext = MyModel;
}
public Model2 MyModel = new Model2();

動的バインドにはBindingクラスを使います。これを、バインド名を指定して new をします。
バインドの方向は「TwoWay」で指定します。
で、コントロールに結びつけるために SetBinding( どのプロパティか、バインドオブジェクト ) で設定しておしまいです。
これは先の「Text=”{Binding LastName, Mode=TwoWay}”」と同じことです。

まあ3行書くのが面倒な場合は、適当なメソッドを作るとよいでしょう。

private void SetBinding( 
    Control ctrl, DependencyProperty prop, 
    string path, BindingMode mode )
{
    Binding bi = new Binding(path);
    bi.Mode = mode;
    ctrl.SetBinding(prop, bi);
}

public Page()
{
    InitializeComponent();

    // 動的にバインディング
    SetBinding(TextName, TextBox.TextProperty, "LastName", BindingMode.TwoWay);
    SetBinding(TextOut, TextBox.TextProperty, "OutName", BindingMode.TwoWay);

    // コマンドも動的に設定
    this.BtnSave.Click += BtnSave_Click;

    // データコンテキストに設定
    this.DataContext = MyModel;
}

配列にしたりして設定っぽくしておけば間違いが少ないと思います。
どっちにせよ、このバインドという代物は、プログラマ側が設定するので、xaml に入れるのは MVVM 的な視点で言えば望ましくありません。

ちなみに、ボタンクリックのイベントのほうは、

this.BtnSave.Click += BtnSave_Click;

な形で簡単に設定できます。
Visual Studio 2008 上で作ると

this.BtnSave.Click += new RoutedEventHandler(BtnSave_Click);

な形を勝手に作ってくれます。
わざわざ new RoutedEventHandler で包む必要はないと思うのですが、どうなんでしょうね?
RoutedEventHandler を継承した独自なハンドラを作ったら、Click に直接入れられないから new しないと駄目とか。

これをUnitTestを使って動かすと、問題なく動きます。

[TestMethod]
public void TestTextAutoBinding()
{
    Model2 model = _page.MyModel;

    Assert.AreEqual("", model.LastName);
    model.LastName = "masuda";
    Assert.AreEqual("masuda", model.LastName);

    /// 動的にバインディングした場合でも即時
    Assert.AreEqual("", model.OutName);
    _page.TestBtnSaveClick();
    Assert.AreEqual("masuda", model.OutName);
}

直接 xaml に書いても、コードから動的にバインドしても同じ動きですね。当たり前ですが。

カテゴリー: 開発 | XAMLの動的バインド はコメントを受け付けていません

Silverlight + MVVM モデルで DataGrid をバインドの落とし穴(その2)

落とし穴シリーズ(にするつもりは無いのですが)の続きです。
本当は、動的バインドの話を書こうと思ったのですが、ItemsSourceプロパティにバインドするときの注意を忘れてました。

DataGrid コントロールのItemsSourceプロパティには、配列やListコレクションなどが渡せます。最近はジェネリックの流行りが功を奏して(かな?)、List<>を渡す場合が多くなっています。
実は IList と List<> の違いどころを間違えるとややこしいことになるのですが、それは別の機会には話します。型チェックのキャストでは List<> の方が不利なんです。型が2つあるからね。

さて、先日のDataGridのソースでは List<> を渡しました。
ページクラスのコンストラクタを改めてみてみると、次のようになっています。

public PageGrid()
{
    InitializeComponent();
    MyModel = new ModelGrid();
    this.DataContext = MyModel;

    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add( new Product(){ ID=&quot;001&quot;, Name=&quot;Silverlight 3&quot; });
    list.Add( new Product(){ ID=&quot;002&quot;, Name=&quot;Visual Studio 2010&quot; });
    list.Add( new Product(){ ID=&quot;003&quot;, Name=&quot;Expression Bend 3&quot; });
    // グリッドに設定
    this.DGrid.ItemsSource = list;
}

ここでは、コントロールのグリッドに直接設定しているのですが、これは MyModel クラスの Items プロパティに設定してもよいはずです。Items プロパティは DataGrid の ItemsSource プロパティにバインドされています。

public PageGridEdit()
{
    InitializeComponent();
    MyModel = new ModelGridEdit();
    this.DataContext = MyModel;

    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add(new Product() { ID = &quot;001&quot;, Name = &quot;Silverlight 3&quot; });
    list.Add(new Product() { ID = &quot;002&quot;, Name = &quot;Visual Studio 2010&quot; });
    list.Add(new Product() { ID = &quot;003&quot;, Name = &quot;Expression Blend 3&quot; });
    MyModel.Items = list ;	★
    
}

変わった部分は★だけです。これを画面で動作確認すると、正常に動きます。
そうなんです。ここまでは、検索の例と同じになります。

ここで、このグリッドを編集できるようにしましょう。
グリッド上で編集することもできますが、ここは定番のテキストボックスを使って編集する方法を考えましょう。

<Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
<StackPanel>
    <TextBox Name=&quot;TextID&quot; Width=&quot;100&quot; Text=&quot;{Binding ProductID, Mode=TwoWay}&quot; />
    <TextBox Name=&quot;TextName&quot; Width=&quot;200&quot; Text=&quot;{Binding ProductName, Mode=TwoWay}&quot;/>
    <Button Name=&quot;BtnAdd&quot; Width=&quot;100&quot; Content=&quot;新規追加&quot; Click=&quot;BtnAdd_Click&quot;/>
    <Button Name=&quot;BtnDel&quot; Width=&quot;100&quot; Content=&quot;削除&quot; Click=&quot;BtnDel_Click&quot;/>
    <Button Name=&quot;BtnUpdate&quot; Width=&quot;100&quot; Content=&quot;更新&quot; Click=&quot;BtnUpdate_Click&quot;/>
    <dt:DataGrid Name=&quot;DGrid&quot; Width=&quot;300&quot; Height=&quot;200&quot;
                 ItemsSource=&quot;{Binding Items, Mode=TwoWay}&quot; 
                 SelectionChanged=&quot;DGrid_SelectionChagned&quot;
                 />
</StackPanel>
</Grid>

TextID と TextName が編集のためのテキストボックスです。これも ProductID と ProductName という名前でバインドしておきます。バインド先は MyModel です。
# 本当は、編集用のProductクラスにバインドするのですが、説明のために MyModel にバインドします。

ボタンは「新規追加」「削除」「更新」ボタンの3つを用意します。実際は、編集している途中はグリッドを動かせないとか、キャンセルができるとか、色々やることがありますが、今は基本だけを押さえておきましょう。

もうひとつ DataGrid コントロールのイベントを追加します。SelectionChangedイベントです。これはグリッドのカーソルが移動したときに発生するイベントで、カーソルを移動したときに、TextID や TextName コントロールの内容を変更します。

3つのボタンのイベントとデータグリッドの選択イベントを記述したのが次のコードです。

/// <summary>
/// 新規追加ボタン
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void BtnAdd_Click(object sender, RoutedEventArgs e)
{
    Product item = new Product();
    item.ID = MyModel.ProductID;
    item.Name = MyModel.ProductName;
    MyModel.Items.Add(item);
}
/// <summary>
/// 削除ボタン
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void BtnDel_Click(object sender, RoutedEventArgs e)
{
    if (this.DGrid.SelectedItem != null)
    {
        Product item = (Product)DGrid.SelectedItem;
        MyModel.Items.Remove(item);
    }
}
/// <summary>
/// 更新ボタン
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void BtnUpdate_Click(object sender, RoutedEventArgs e)
{
    if (this.DGrid.SelectedItem != null)
    {
        Product item = (Product)DGrid.SelectedItem;
        item.ID = MyModel.ProductID;
        item.Name = MyModel.ProductName;
    }
}
/// <summary>
/// カーソル移動
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void DGrid_SelectionChagned(object sender, SelectionChangedEventArgs e)
{
    if (DGrid.SelectedItem != null)
    {
        Product item = (Product)DGrid.SelectedItem;
        MyModel.ProductID = item.ID;
        MyModel.ProductName = item.Name;
    }
}

データグリッドで選択行を取得する以外は、MyModelのプロパティを使います。ここの部分が MVVM の原則をはみ出しているような気がする場合は、MyModel に SelectionChanged イベントを作るか、OnSelectionChangedメソッドを作って、それを呼び出せばよいでしょう。

さて、モデルクラスのほうは ProductID と ProductName プロパティを追加すればおわりです。

/// <summary>
/// モデルクラス
/// </summary>
public class ModelGridEdit : INotifyPropertyChanged
{
/// <summary>
/// グリッドのデータ
/// </summary>
private IList _Items;
public IList Items
{
    get { return _Items; }
    set
    {
        if (_Items != value)
        {
            _Items = value;
            OnPropertyChanged(&quot;Items&quot;);
        }
    }
}

private string _ProductID ;
public string ProductID
{
    get { return _ProductID; }
    set
    {
        if (_ProductID != value)
        {
            _ProductID = value;
            OnPropertyChanged(&quot;ProductID&quot;);
        }
    }
}

private string _ProductName ;
public string ProductName
{
    get { return _ProductName; }
    set
    {
        if (_ProductName != value)
        {
            _ProductName = value;
            OnPropertyChanged(&quot;ProductName&quot;);
        }
    }
}

#region INotifyPropertyChanged メンバ
/// <summary>
/// プロパティ変更時のイベント
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
    if (PropertyChanged != null)
        PropertyChanged(this, e);
}
protected virtual void OnPropertyChanged(string name)
{
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(name));
}
#endregion
}

これを画面で実行すると、初期画面にリストが表示されるのですが。。。
新規追加ボタンと削除ボタンがうまく動作しません。
テキストボックスにIDとNameを入力して、追加ボタンを押してもグリッドが更新されません。ブラウザを一旦、最小化させたり、再表示させてもダメです。
しかし、この状態で別のグリッドをクリックすると、行が追加されます。変な動きですね。バグみたいです。

ちなみに、行をたくさん追加すると、スクロールバーを動かしたときにグリッドの表示が更新されます。変な動きですね。

「これはDataGridの不具合だ!」

と思ったのは私だけではないでしょう!

が、違うのです。実はバグではありません。ItemsSource プロパティの設定の仕方が悪いのです。
さっき、ItemsSource プロパティに入れたのは List<> のコレクションでした。そして、List<> コレクションの中身のオブジェクトは当然ながらコレクションの子項目になります。
あれ?先のエントリを読んでいる方は気づいたかもしれません。
そうです。TextコントロールやDataGridコントロールを直接バインドした場合には、それぞれのプロパティの変更は追随しないのです。画面の表示時に読みだすため、一見「バインドが遅い」という感じがするのです。

というわけで、List<> コレクションで自動的に DataGrid を更新することはできません。
困った。
というわけで、MSDN をよく読んでみると、「ObservableCollection<> コレクションを使え」となっています。なんでしょう?この ObservableCollection コレクションってのは?

実は ObservableCollection はリストへの追加や削除のイベントが拾えるコレクションです。というわけで、ObservableCollectionコレクションを使ってリストを作ると、このリストに追加/削除すると、元々のDataGridにイベントが飛ぶんですね。

では早速仰せの通り変更してみましょう。

public PageGrid()
{
    InitializeComponent();
    MyModel = new ModelGrid();
    this.DataContext = MyModel;

    // コレクションを作成
    ObservableCollection<Product> list = new ObservableCollection<Product>(); ★
    list.Add( new Product(){ ID=&quot;001&quot;, Name=&quot;Silverlight 3&quot; });
    list.Add( new Product(){ ID=&quot;002&quot;, Name=&quot;Visual Studio 2010&quot; });
    list.Add( new Product(){ ID=&quot;003&quot;, Name=&quot;Expression Bend 3&quot; });
    // グリッドに設定
    MyModel.Items = list ;
}

修正する箇所は★の一か所だけです。使い慣れない名前ですが、まあ、インテリセンスを利用して打ち込んでみてください。
これを動かすと、おお動くじゃないですか!追加も削除もOK.グリッドでカーソルをぽちぽち動かしても大丈夫。「これはDataGridの不具合だ!」なんて思ってごめんね、Silverlight君。

ってなわけで、これでめでたしめでたし。ってなことになるはずですが、そう、忘れていましたね。
更新ボタンの処理です。

グリッドを選択して、テキストで編集して、更新ボタンをクリック。
さて、グリッドの表示は、変わりません!

なにこれぇ!MVVMって全然使えないジャン。データグリッドを更新してくれなくちゃ意味ないよ。
「これこそDataGridの不具合だ!」
って思って、投げ出したくなります。実際、私も投げ出したくなりました。データグリッドを無理矢理更新しようと思って、Remove/Insertをしたり、一旦ItemsSourceにnullを入れて改めてリストを代入したり、と。
巷のブログでも似たようなことで困っている人がいらっしゃいます。

実はこれはObservableCollectionコレクションに渡すオブジェクトが悪いのです。
Productクラスは次のように単純なデータ型です。

public class Product
{
    public string ID { get; set; }
    public string Name { get; set; }
}

実は、データを更新したときにObservableCollectionコレクションが受け取れるように(ItemsSourceプロパティが受け取れるように)、ここに INotifyPropertyChanged インターフェースが必要なのです。
う~ん。これに気付けってってのが難しいのですが、落とし穴ですね。

public class Product: INotifyPropertyChanged
{
private string _ID;
public string ID
{
    get { return _ID; }
    set
    {
        if (_ID != value)
        {
            _ID = value;
            OnPropertyChanged(&quot;ID&quot;);
        }
    }
}

private string _Name;
public string Name
{
    get { return _Name; }
    set
    {
        if (_Name != value)
        {
            _Name = value;
            OnPropertyChanged(&quot;Name&quot;);
        }
    }
}

#region INotifyPropertyChanged メンバ
/// <summary>
/// プロパティ変更時のイベント
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
    if (PropertyChanged != null)
        PropertyChanged(this, e);
}
protected virtual void OnPropertyChanged(string name)
{
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(name));
}
#endregion
}

こんな風にProductクラスを書き換えます。定番となったOnPropertyChangedはそのままコピー&ペーストしてください。
# プロパティ作成のスペニットがあればいんですが、作り方がよくわからないし。いつもコピー&ペーストして置換をしています。まあ、後でスペニットを作りましょう。
# つーか、この書き方じゃない別に方法を考えましょう。Cの#defineマクロみたいなやつを。

2つの落とし穴があるわけですが、これを修正するとDataGridは思ったように動きます。多分Detailコントロールなんかはこのあたりを自動化しているのでしょう。

落とし穴をまとめると、

・ItemsSourceプロパティにはObservableCollection<>を使え!
・ObservableCollectionコレクションの型にはINotifyPropertyChangedインターフェースを使え!

です。

やれやれ(空条承太郎風ではなくてカート・ヴォネガット風に)。

カテゴリー: 開発 | 2件のコメント

Silverlight + MVVM モデルで DataGrid をバインドの落とし穴

動的バインドの話を書こうと思ったのですが、DataGrid のバインドの落とし穴の件を書くことにします。
多分、検証結果を具体的にみるほうが「何故だめなのか?」がわかりやすいので。

先に注意しておきますが、ここに書くのは「やってはいけない」方法です。

さて、ItemSource プロパティにバインドする場合は、前回書いたように即時実行が行われます。
これを「楽だから」という理由で、DataContextを使ってコントロール自身にバインドしてみましょう。

xaml ファイルを次のように設定します。

<Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
<StackPanel>
    <dt:DataGrid Name=&quot;DGrid&quot; Width=&quot;300&quot; Height=&quot;200&quot;
                 DataContext=&quot;{Binding DGridData, Mode=TwoWay}&quot; />
    <Button Name=&quot;BtnSearch&quot; Width=&quot;100&quot; Content=&quot;検索&quot; Click=&quot;BtnSearch_Click&quot;/>
</StackPanel>
</Grid>

DataContext=”{Binding DGridData, Mode=TwoWay}” しているところが、バインドです。

モデルクラスはこんな感じになります。

/// <summary>
/// モデルクラス
/// </summary>
public class ModelGrid2 : INotifyPropertyChanged
{
/// <summary>
/// グリッドのデータ
/// </summary>
private DataGrid _DGridData = new DataGrid();
public DataGrid DGridData
{
    get { return _DGridData; }
    set
    {
        if (_DGridData != value)
        {
            _DGridData = value;
            OnPropertyChanged(&quot;DGridData&quot;);
        }
    }
}

#region INotifyPropertyChanged メンバ
/// <summary>
/// プロパティ変更時のイベント
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
    if (PropertyChanged != null)
        PropertyChanged(this, e);
}
protected virtual void OnPropertyChanged(string name)
{
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(name));
}
#endregion
}

データグリッド本体に対してバインドするので、

public DataGrid DGridData

のようにプロパティを書きます。バインド名は「DGridData」です。
検索ボタンを押下したときの処理は、次のようにモデルクラスのほうを使います。

private void BtnSearch_Click(object sender, RoutedEventArgs e)
{
    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add(new Product() { ID = &quot;001&quot;, Name = &quot;Silverlight 3&quot; });
    list.Add(new Product() { ID = &quot;002&quot;, Name = &quot;Visual Studio 2010&quot; });
    list.Add(new Product() { ID = &quot;003&quot;, Name = &quot;Expression Bend 3&quot; });
    list.Add(new Product() { ID = &quot;004&quot;, Name = &quot;New Application 1&quot; });
    list.Add(new Product() { ID = &quot;005&quot;, Name = &quot;New Application 2&quot; });
    list.Add(new Product() { ID = &quot;006&quot;, Name = &quot;New Application 3&quot; });
    // グリッドに設定(バインド版)
    MyModel.DGridData.ItemsSource = list;
}

先ほどバインドしたのがコントロール自体なので、

MyModel.DGridData.ItemsSource = list;

な形でアクセスをします。
一見、これで十分動くような気もしますが、動きません。
画面上で動かしても、データグリッドの内容が更新されません。
また次のようなUnitTestでもエラーになります。

[TestMethod]
public void TestGridData()
{
    ModelGrid2 model = _page.MyModel;
    Assert.AreEqual(3, ((IList)_page.DGrid.ItemsSource).Count);

    List<Product> list = new List<Product>();
    list.Add(new Product() { ID = &quot;001&quot;, Name = &quot;Silverlight 3&quot; });
    list.Add(new Product() { ID = &quot;002&quot;, Name = &quot;Visual Studio 2010&quot; });
    list.Add(new Product() { ID = &quot;003&quot;, Name = &quot;Expression Bend 3&quot; });
    list.Add(new Product() { ID = &quot;004&quot;, Name = &quot;New Application 1&quot; });
    list.Add(new Product() { ID = &quot;005&quot;, Name = &quot;New Application 2&quot; });
    list.Add(new Product() { ID = &quot;006&quot;, Name = &quot;New Application 3&quot; });
    model.DGridData.ItemsSource = list;

    Assert.AreEqual(6, ((IList)model.DGridData.ItemsSource).Count);
    // ↓はバインドされずにエラーになる。
    // Assert.AreEqual(6, ((IList)_page.DGrid.ItemsSource).Count);
}

最後の Assert.AreEqual のところで、((IList)_page.DGrid.ItemsSource).Count は 3 を返します。
これは既にデータグリッドに設定したデータ数で、バインドが正常に働いていないように見えます。

ですが、これはバインドが働いていないのではなくて、バインドの仕方が間違っているのです。
バインドはコントロール自身となっているので、コントロール自体の変更は通知しますが、コントロールが持つ各種プロパティの変更には応答しません。
通知しないものだから、データグリッドの画面の更新も行われません。ItemsSourceプロパティにバインドしている場合は、正常に画面が更新されます。
これを通知するためには、モデルクラスのItemsSourceプロパティのほうをoverrideして、ObservableCollectionを使って、その通知を受けたものを、バインド先のコントロールに更に通知する、というような処理をする必要があります。これは、無駄です。こんな変なことをしなくてはいけないのは、コントロール自体にバインドをしたためであって、やってはいけないことなのです。まぁ、やってもいいけど、痛い目に会うという余計にややこしいことになります。

というわけで、「DataContext」を使ってコントロールを直接バインドしていはいけません、という話です。

さて、余談ですがここの DataContext 属性は何に使うのでしょうか?

実は先の PageGrid クラスのコンストラクタで DataContext にモデルクラスを設定していました。つまり、DataContext はモデルの元ネタを設定して、それぞれのプロパティ(TextプロパティやItemsSourceプロパティなど)にモデルの各プロパティをバインドするのです。

例では元のページクラスで DataContext を設定しましたが、それぞれのコントロールにデータ/モデルを設定することが可能です。例えば、ページクラスは MyModel クラスを使い、データグリッドは MyGirdModel を使うという使い分けが可能です。
このような場合には、DataGrid の DataContext に直接記述します。

<Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
<StackPanel>
    <dt:DataGrid Name=&quot;DGrid&quot; Width=&quot;300&quot; Height=&quot;200&quot;
                 DataContext=&quot;{Binding DataMyModel, Mode=TwoWay}&quot;
                 ItemsSource=&quot;{Binding Items, Mode=TwoWay}&quot; />
    <Button Name=&quot;BtnSearch&quot; Width=&quot;100&quot; Content=&quot;検索&quot; Click=&quot;BtnSearch_Click&quot;/>
</StackPanel>
</Grid>

このように、DataContext プロパティと ItemsSource プロパティにバインドを行います。DataContext のほうは一度設定したモデルクラス自身を切り替えないのであれば、Mode を TwoWay にする必要はありません。

このあたり、ASP.NET のデータバインドと同じなので合わせて参考にしてください。

カテゴリー: 開発 | Silverlight + MVVM モデルで DataGrid をバインドの落とし穴 はコメントを受け付けていません

Silverlight + MVVM モデルで DataGrid をバインド

昨日の続きです。

本当は「View の切替/換装ができる」ってのを書きたいのですが、その前にデータバインドのところを押さえないと訳がわからなくなるので、書いておきます。まあ、覚書ということで。

Silverlight にもグリッドで表示するための DataGrid コントロールがあります。Windows で言う DataGridView であったり、ASP.NET だと名前が違ったりと色々ありますが、まぁ似たような動きをします。
この DataGrid には、データベースから検索した DataTable や List のコレクションなどを指定して、自動的にグリッドに表示できます。

忘れちゃいけないのは「System.Windows.Control.Data.dll」を参照設定することですね。DataGridのコントロールは System.Windows.Control namespace にあるのですが、DLL は System.Windows.Control.Data という。
準備として xaml の namespace を付けます。
下記では「dt」という名前空間を設定しています。

<UserControl x:Class=&quot;SampleTextBinding.PageGrid&quot;
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:dt=&quot;clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data&quot;
    Width=&quot;400&quot; Height=&quot;300&quot;>
    <Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
  
    </Grid>
</UserControl>

そして、データグリッドを表示させるために「dt:DataGrid」を配置します。

<UserControl x:Class=&quot;SampleTextBinding.PageGrid&quot;
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:dt=&quot;clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data&quot;
    Width=&quot;400&quot; Height=&quot;300&quot;>
    <Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
        <StackPanel>
            <dt:DataGrid Name=&quot;DGrid&quot; Width=&quot;300&quot; Height=&quot;200&quot;/>
        </StackPanel>
    </Grid>
</UserControl>

ここでは名前を「DGrid」にしてあるので、データグリッドのコントロールを使って直接指定する場合は、

public PageGrid()
{
    InitializeComponent();
    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add( new Product(){ ID=&quot;001&quot;, Name=&quot;Silverlight 3&quot; });
    list.Add( new Product(){ ID=&quot;002&quot;, Name=&quot;Visual Studio 2010&quot; });
    list.Add( new Product(){ ID=&quot;003&quot;, Name=&quot;Expression Bend 3&quot; });
    // グリッドに設定
    this.DGrid.ItemsSource = list;

}
public class Product {
    public string ID { get; set; }
    public string Name { get; set; }
}

を記述します。

ページを表示したときに既に表示されるようにする場合は、Page クラスのコンストラクタに。
通常は、ボタンなどの何らかのアクションを起こした後の検索結果を表示すると思います。
この場合はボタンのイベントに記述します。

private void BtnSearch_Click(object sender, RoutedEventArgs e)
{
    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add(new Product() { ID = &quot;001&quot;, Name = &quot;Silverlight 3&quot; });
    list.Add(new Product() { ID = &quot;002&quot;, Name = &quot;Visual Studio 2010&quot; });
    list.Add(new Product() { ID = &quot;003&quot;, Name = &quot;Expression Bend 3&quot; });
    list.Add(new Product() { ID = &quot;004&quot;, Name = &quot;New Application 1&quot; });
    list.Add(new Product() { ID = &quot;005&quot;, Name = &quot;New Application 2&quot; });
    list.Add(new Product() { ID = &quot;006&quot;, Name = &quot;New Application 3&quot; });
    // グリッドに設定
    this.DGrid.ItemsSource = list;
}

この検索ボタン(BtnSearch)をクリックすると、グリッドが再描画されます。

データグリッドの中身にバインドする場合は、ItemsSource プロパティにバインドします。
先の「落とし穴」と同様にコントロール自身にバインドしてはいけません。

<Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
    <StackPanel>
        <dt:DataGrid Name=&quot;DGrid&quot; Width=&quot;300&quot; Height=&quot;200&quot;
         ItemsSource=&quot;{Binding Items, Mode=TwoWay}&quot; />
        <Button Name=&quot;BtnSearch&quot; Width=&quot;100&quot; Content=&quot;検索&quot; Click=&quot;BtnSearch_Click&quot; />
    </StackPanel>
</Grid>

バインドする時の記法はテキストボックスのときと同じです。違いはバインド先が「ItemsSource」となることです。

モデルクラスもテキストの場合と似たようなものですが、プロパティの型が違います。

/// <summary>
/// モデルクラス
/// </summary>
public class ModelGrid : INotifyPropertyChanged
{
    /// <summary>
    /// グリッドのデータ
    /// </summary>
    private IList _Items;
    public IList Items
    {
        get { return _Items; }
        set
        {
            if (_Items != value)
            {
                _Items = value;
                OnPropertyChanged(&quot;Items&quot;);
            }
        }
    }
   
    #region INotifyPropertyChanged メンバ
    /// <summary>
    /// プロパティ変更時のイベント
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, e);
    }
    protected virtual void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
    #endregion
}

プロパティ名を ItemsSource としてもよいのですが、ここでは「Items」という名前を使います。
先の xaml を見るとわかるのですが、バインドするときに名前が違います。

つまり、コントロールで使う名前とモデルクラスで使う名前と異なってもいいんです。これは当然で、View には同じコントロールを複数貼り付けることもあるので、バインド先のモデルの名前を別にすることが可能です。
これにより Model と View との分離ができます。

さて、Model と View を結びつけるための DataContextプロパティを指定すればおしまいです。

public partial class PageGrid : UserControl
{
    public ModelGrid MyModel;

    public PageGrid()
    {
        InitializeComponent();
        MyModel = new ModelGrid();
        this.DataContext = MyModel;

これも便宜上、Model のオブジェクトを Page クラス内に作っていますが、本当は別の場所に作ります。

さて、バインドの状態をSilverlightのUnitTestを使って動かします。

[TestMethod]
public void TestBindItems()
{
    ModelGrid model = _page.MyModel;

    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add(new Product() { ID = &quot;001&quot;, Name = &quot;Silverlight 3&quot; });
    list.Add(new Product() { ID = &quot;002&quot;, Name = &quot;Visual Studio 2010&quot; });
    list.Add(new Product() { ID = &quot;003&quot;, Name = &quot;Expression Bend 3&quot; });
    list.Add(new Product() { ID = &quot;004&quot;, Name = &quot;New Application 1&quot; });
    list.Add(new Product() { ID = &quot;005&quot;, Name = &quot;New Application 2&quot; });
    list.Add(new Product() { ID = &quot;006&quot;, Name = &quot;New Application 3&quot; });
    // グリッドに設定
    Assert.AreEqual(6, list.Count);
    model.Items = list;
    Assert.AreEqual(6, model.Items.Count); 
    Assert.AreEqual(6, ((IList)_page.DGrid.ItemsSource).Count); ★
}

このUnitTestは通ります。
特に★の部分に注目してください。モデルクラスの Items プロパティに設定した時に即時、データグリッドの ItemsSource プロパティに値が入っていることがわかります。イコール、画面にも即時反映されるということです。

実は、これを DataContextに直接コントロールをバインドさせると、即時にはなりません。画面を描画するときにバインドが発生するので、UnitTestは常にエラーになってしまいます。
画面で動かす分には問題がない、ように見えますが、いろいろ問題が出るので ItemsSource に直接バインドするのがよいでしょう。

カテゴリー: 開発 | Silverlight + MVVM モデルで DataGrid をバインド はコメントを受け付けていません

Silverlight MVVM の落とし穴

少し仕事のペースが落ち着いてきたので情報を流しておきます。

現在使っているフレームワークが SLExtensions の DLL を使っているので、なんじゃろ?という形で MVVM モデルってのがあるのことを知りました。以前から MVC モデルを利用しているし、.NET ラボの9月の勉強会(後ほど資料をアップする予定です)でも話しましたが、MVVM であろうと、MVC であろうと、「MV○」ってモデルには違いないので、最新って訳でもありません。
実装上はあれこれあるんですが、まぁ、タイトル通り「落とし穴」があるんです。

SLExtensionsを使ったM-V-VMパターン実装
http://d.hatena.ne.jp/coma2n/20090217/1234881006

を見ていくつかページを探っていきました。この方、業務用のアプリ作成に精通しているらしく MVC モデルの根本的なところを理解していらっしゃいます。VB 6.0 なんかが出てくるところが好感が持てます!
要は、
・ソフトウェア工学なところ(理学部的なところ)
・現場での実装/作業場の問題(工学部的なところ)
の問題があります。文系の方には、理学部と工学部の違いがよくわからないかもしれませんが、

「理論」と「実践」という比較で、理論が「理学部」、実践が「工学部」ってことです。
ソフトウェア工学の場合「工学」を名乗っているところもあるのですが、純粋に数学的なアルゴリズムの問題もあって、この理学/工学が混在しています。

画像認識の論文集を買ったのですが、理論的なところだけでなく、実装上の問題やスピードの問題も扱っているところが工学的ですね。収束スピードとか。

さて、MVVM パターンを実装した SLExtensions の DLL ですが、これが無くても MVVM パターンは実装可能です。というか個人的には SLExtensions を使わない方がコードがすっきりします。なんとなくですが、このあたり「理論としてのパターン」→「パターンをそのまま実装」している「理論」領域に留まっており、実際に作業量など(人的スキルも込み)を考慮した本来の「工学」に至っていない気がします。

# 余談ですが、Microsoft の実装面は、多少(?)ごちゃごちゃな面も含めて私は好きです。
# Java の整理されちゃったコードパターンよりも、C# のごちゃごちゃさはこの手のパターンを実装するときに、「パターン」≠「実装パターン」とすることが可能なのです。このあたり、C++ の template や #define マクロに通じるものがあるんですが。残念ながら C# はプリプロセッサを実装しないときたもんだ。

ええと、最初に言っておきますが、私としては MVVM モデルをこのような実装の仕方にするのは反対です。
というのも、ASP.NET や Silverlight の場合、既にVisual Studioの自動生成機能によって、コントロール自身がバインドされているので、Textプロパティ等に独自にバインディングする意味がありません。ViewとModelを分けたいのであれば、Silverlight に既に実装してあるものを利用するが分りやすいと思います。

ただし、データベースとの自動バインディングを推し進めた場合(いわゆるノンコーディングの場合)には、この実装が有効です。この話は別な機会に。というか、本当は.NETラボの勉強会で話したかったネタです。

と、やっとここから本題です。

■Textプロパティへのバインド

TextBoxコントロールに表示するTextプロパティのバインドを行います。
まず、xaml にバインドするポイントを記述します。

Page.xaml

<Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
    <StackPanel>
        <TextBox Name=&quot;TextName&quot; Width=&quot;100&quot; Text=&quot;{Binding LastName, Mode=TwoWay}&quot; />
        <Button Name=&quot;BtnSave&quot; Width=&quot;100&quot; Content=&quot;保存&quot; Click=&quot;BtnSave_Click&quot; />
        <TextBox Name=&quot;TextOut&quot; Width=&quot;100&quot; Text=&quot;{Binding OutName, Mode=TwoWay}&quot; />
    </StackPanel>
</Grid>

Text=”{Binding LastName, Mode=TwoWay}”

のところがそれですね。「LastName」という名前でバインドされています。

さて、これが何処にバインドされるかというと、モデルクラスのプロパティになります。
バインドが双方向「TwoWay」なので、INotifyPropertyChangedインターフェースを実装します。INotifyPropertyChangedインターフェースは、お約束のコードがあるので、そのまま貼り付けます。
SLExtensions を使う場合は、ここのコードが既に実装済みです。

便宜上 Page.xaml.cs に入れていますが、どこにおいても構いません。

/// <summary>
/// モデルクラス
/// </summary>
public class Model : INotifyPropertyChanged
{
    /// <summary>
    /// 入力値
    /// </summary>
    private string _LastName = &quot;&quot;;
    public string LastName
    {
        get { return _LastName; }
        set
        {
            if (_LastName != value)
            {
                _LastName = value;
                OnPropertyChanged(&quot;LastName&quot;);
            }
        }
    }

    /// <summary>
    /// 保存ボタン押下時の出力値
    /// </summary>
    private string _OutName = &quot;&quot;;
    public string OutName
    {
        get { return _OutName; }
        set
        {
            if (_OutName != value)
            {
                _OutName = value;
                OnPropertyChanged(&quot;OutName&quot;);
            }
        }
    }
    #region INotifyPropertyChanged メンバ
    /// <summary>
    /// プロパティ変更時のイベント
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, e);
    }
    protected virtual void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
    #endregion
}

これで、ViewとModelの準備ができました。
ただ、このままではダメで、これを結び付ける処理を入れます。
Page.xaml.cs

public partial class Page : UserControl
{
    public Page()
    {
        InitializeComponent();
        this.DataContext = MyModel;
    }
    public Model MyModel = new Model();

データバインドは、InitializeComponent の前に入れるのか、後ろに入れるのか決まってはいませんが、一応、後ろに入れます。何かと InitializeComponent の中で色々やってくれるので。
MyModel フィールドが public になっているのは、UnitTest のためです。本来、Model は View や ViewModel と切り離すのが目的なので、普通ここに Model オブジェクトの実体は置きません。
実行を確認してはいませんが、SLExtensions を使う場合は、InitializeComponent の前に入れる必要があるようです。

さて、これで MVVM の実装が済みました。

動かすところは、保存ボタンを押したときで、


<Button Name=&quot;BtnSave&quot; Width=&quot;100&quot; Content=&quot;保存&quot; Click=&quot;BtnSave_Click&quot; />

の BtnSave_Click イベントが発生したときです。
実は、イベント自体もバインドが可能(SLExtensionsでは、独自にバインドします)なのですが、ここでは Page.xaml.cs のほうに記述します。

private void BtnSave_Click(object sender, RoutedEventArgs e)
{
    MyModel.OutName = MyModel.LastName;
}

これは MyModel の LastName の値(文字列)を OutName に代入しているところです。
普通は、

TextOut.Text = TextName.Text;

な感じでテキストボックスのTextプロパティを参照するところですが、ここではModelクラスのプロパティを利用します。
Textプロパティのほうが、分りやすいと言えば分りやすいのですが、ここはMVVMモデルの弊害ですね。Modelクラスを使うと直観的でないのが残念です。ただ、慣れると、

・Viewクラスの挙動を知らなくてよい。
・シリアライズ等のデータに関するメソッドを Model クラスに集約できる
・単体テストがしやすい

という利点があります。特に単体テストは便利です。
普通 Silverlight の画面のテストは、ひとが手でマウスをクリックしながらやると思うのですが、Model クラスに分離すると、通常の NUnit のようなテストツールが使えます。
もっとも、Silverlight 用のテストツールもあるので、それを利用するのもテです。

先のModelクラスを動かすと、画面のTextBoxを触っていないのに、ボタンをクリックするときちんと文字列が表示されます。これが「バインド」の効果で、先に xaml にした Binding と Model のプロパティが結びついているために、このように動きます。

■バインドの落とし穴

さて、ここからが本題です。
このバインドの機能を使った MVVM モデルですが、経験が浅い(?)と Model の作成に閉口します。
たぶん、Visual Studio で画面をいっぺんい作っていると画面とイベントの結びつけを意識しないので(意識しない造りになっているので)、このあたりを分離するのが困難なのだと思います。

そういう場合は、素直に MVVM モデルを諦めて、先の Page.xml.cs に直接コントロール経由で値を操作したほうが無難です。もともと Silverlight や ASP.NET が MVC モデルで出来ているので、あえて厳しい制限された形で MVVM モデルを導入する必要はありません。

が、現場の状況によりけりでして、どうしても Model を作らないと駄目になったときに、思わず「コントロールそのものをバインドしてしまおう」という発想になります。
これが落とし穴です。

では実際に、コントロールをバインドしてみましょう。

Page.xaml

<Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
    <StackPanel>
        <TextBox Name=&quot;TextBoxIn&quot; Width=&quot;100&quot; DataContext=&quot;{Binding TBoxInName, Mode=TwoWay}&quot; />
        <TextBox Name=&quot;TextBoxOut&quot; Width=&quot;100&quot; DataContext=&quot;{Binding TBoxOutName, Mode=TwoWay}&quot; />
        <Button Name=&quot;BtnSave&quot; Width=&quot;100&quot; Content=&quot;保存&quot; Click=&quot;BtnSave_Click&quot; />
    </StackPanel>
</Grid>

上記のように DataContext にバインドしてしまいます。
Page.xaml.cs に記述するときは TextBoxIn コントロールを直接参照できるのですが、Model からは TextBoxIn が見れません。ですので、バインドしてしまうわけです。

/// <summary>
/// モデルクラス
/// </summary>
public class Model : INotifyPropertyChanged
{

    private TextBox _TBoxOutName = new TextBox();
    public TextBox TBoxOutName
    {
        get { return _TBoxOutName; }
        set
        {
            if (_TBoxOutName != value)
            {
                _TBoxOutName = value;
                OnPropertyChanged(&quot;TBoxOutName&quot;);
            }
        }
    }
    private TextBox _TBoxInName = new TextBox();
    public TextBox TBoxInName
    {
        get { return _TBoxInName; }
        set
        {
            if (_TBoxInName != value)
            {
                _TBoxInName = value;
                OnPropertyChanged(&quot;TBoxInName&quot;);
            }
        }
    }
   
}

Model クラスでは、TBoxOutName プロパティで受けます。
TextBox クラスになるので、「嬉しい」ことに

TBoxOutName.Text

なって形でTextプロパティにアクセスできます。
なので、保存ボタンをクリックしたときは以下のようになります。

private void BtnSave_Click(object sender, RoutedEventArgs e)
{
 MyModel.TBoxOutName.Text = MyModel.TBoxInName.Text;
}

一見すると、何も変わっていないように見えますが、テキストボックスのコントロールを直接参照しています。コントロールなので、Textプロパティを直接扱える、という利点があるのですが。
利点というか難点というか、MVVM モデル上のルール違反ですよね。これは。デザインに関わるところをごっそり持ってきて Model クラスで操作してしまう、ってのが間違いです。

で、これで動作させると、普通に動きます。先のTextプロパティにバインドしたときと同じように保存ボタンをクリックすると、画面のテキストボックスにバインドされます。
なので、どっちでもいいじゃん、めでたしめでたし(ルール違反は別として)に思えるのですが、実は違います。
このコントロールのバインド。一見、TBoxOutName.TextプロパティがテキストボックスのTextプロパティに直接バインドできているように思えますが「違います」。画面を表示しているときに、テキストボックスがTextプロパティに設定しているだけなのです。
これは、Silverlight の UnitTest をした時に判明します。

    model.TBoxInName.Text = "masuda";
    Assert.AreEqual("masuda", model.TBoxInName.Text);
    _page.TestBtnSaveClick();
    Assert.AreEqual("masuda", model.TBoxOutName.Text);
    // 即時バインドされない
    Assert.AreEqual("masuda", _page.TextBoxOut.Text); ★

実は★のところでエラーになります。
モデルのプロパティ(model.TBoxOutName.Text)は「masuda」が設定されていますが、直接テキストボックスのプロパティ(TextBoxOut.Text)を参照したときには空白が返ります。
しかし、画面で動かすと問題はありません。きちんと、TextBoxOutコントロールに「masuda」が表示されます。
なぜでしょうか?

■バインドは即時行われる

MVVM モデルの実装で使われる OnPropertyChanged メソッドですが、内部でキューなどを利用しているわけではなく(実装上、そういうバインドもありでしょうが)、Silverlight の場合は即時実行(関数コール型)になっています。

なので、シーケンスとしては、

1.Modelに値を設定する
2.ModelプロパティがOnPropertyChagnedを呼び出す。
3.ViewのProeprtyChangedイベントが実行される。
4.Viewのプロパティに値が設定される。

のようにシーケンシャルに動きます。

が、コントロールをバインドした場合には、

・コントロール自身の変更ではイベントが発生するのですが、
・コントロールのプロパティ(Textプロパティなど)ではイベントは発生しません。

当たり前といえば当たり前ですが、バインドしたオブジェクトに対してのみバインドが発生します。オブジェクトが持っているフィールドや子のオブジェクトを変更しても、イベントは発生しません。

ここが「落とし穴」です。

なので、コントロールに対してバインドを行った場合、一見、バインドがうまく動いているように見えますが、実は全然役に立っていません。

シーケンスとしては、

1.ModelのコントロールのTextプロパティに値を設定する。
2.この時点では、バインドが走らず、Viewのコントロールには古い値が残ったまま。
3.画面を再描画するイベントが走る(ボタンイベントなど)。
4.表示時にViewのコントロールがTextプロパティを参照する。
5.ModelのコントロールのTextプロパティの値を取得する。
6.ViewのコントロールのTextプロパティに値が設定される。
7.ViewのコントロールのTextプロパティが画面に表示される。

という順序になっており、UnitTest を動かしたときには、2で値を取得してNG。だが、画面は7の時点で表示されるからOKっていう具合なのかな、と。

まぁ、どっちにせよ、↓な形でコントロールを直接バインドするのはルール違反ですね。副作用も大きいし、避けておきたい落とし穴です。

 

        <TextBox Name=&quot;TextBoxIn&quot; Width=&quot;100&quot; DataContext=&quot;{Binding TBoxInName, Mode=TwoWay}&quot; />
カテゴリー: 開発 | Silverlight MVVM の落とし穴 はコメントを受け付けていません