開発記その 11 - InvokeMethodAction に引数 -
さて、開発記その1 で書いていた思うところあって作った InvokeMethodAction ですが、その思うところをようやっと実装しました。まぁ、実装自体は結構前にやっていて、ブログに書いてなかったのですが・・・。
で、思うところってなんやったのって話ですが、 InvokeMethodAction で実行されるメソッドに引数を渡したかったのです。たとえば、検索ボックスが一つだけの画面で、検索内容をバリデーションしなくっても OK な場合とかは、わざわざ VM にプロパティ作るのがめんどいですよね。引数に渡ってきてくれればうれしいわけで。っていうのを作りました。
書くコードは以下のような感じ。
<localInteractivity:InvokeMethodAction TargetObject="{Binding DataContext, ElementName=Self}" MethodName="Search"> <localInteractivity:Parameter Value="{Binding Path=Text, ElementName=SearchTextBox}" /> </localInteractivity:InvokeMethodAction>
public void Search(string searchPattern) { //.... }
Parameter を複数並べたら上から順番に引数に渡されます。楽ちんですね。
実装は InvokeMethodAction に ParameterCollection ってプロパティを追加してます。で、この ParameterCollection は DependencyObjectCollection
//InvokeMethodAction... public class InvokeMethodAction : intera.TargetedTriggerAction<object>, INotifyActionCompleted { public InvokeMethodAction() { this.Parameters = new ParameterCollection(); } public ParameterCollection Parameters {get; private set;} //ParameterCollection public class ParameterCollection : DependencyObjectCollection<ParameterBase> {
で、ポイントはこの DependencyObjectCollection
データを表示し、場合によってはユーザーによるデータの変更を許可するターゲット UI プロパティ。ターゲットとしては、FrameworkElement の任意の DependencyProperty を使用できます。Silverlight 4 では、次の場合、ターゲットとして DependencyObject の DependencyProperty も使用できます。
・DependencyObject が FrameworkElement のプロパティの値である。
http://msdn.microsoft.com/ja-jp/library/cc278072(v=VS.95).aspx
・DependencyObject が FrameworkElement のプロパティ (Resources プロパティなど) の値であるコレクション内に存在する。
・DependencyObject が DependencyObjectCollection(Of T) 内に存在する。
もちろん ParameterBase クラスも DependencyObject を継承しています。
public abstract class ParameterBase : DependencyObject {
あと、指定された値を引数の型に変換する部分ですが、 nRoute から拝借しました。 XAML をゴリゴリ生成して、型変換を実行してます(w
※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/56064
開発記その 10 - Action の後に Action -
のんびりと作ってるわけですが、やっとアカウントの作成に取り掛かりました。
アカウントはダイアログあげて、そのダイアログの中で作ろうと思ったわけです。で、ダイアログが閉じたら親画面にあるアカウントの一覧にアカウントが反映されるって寸法です。
で、壁にぶち当たりました。 ViewModel からダイアログってどう呼び出そうかと。なんかまた、画面遷移みたいな形にしようかとも思ったのですが、あまり美味しくなさそうなんで、 Action で ShowDialogAction ってのを作って、呼び出し・表示することにしました。
で、壁にぶち当たりました。ダイアログが閉じたときに、先に書いたように一覧に反映させたいので親画面の ViewModel のメソッドを呼び出したいのです。ダイアログでの結果を、一覧に反映させるだけならオブジェクトを引き回してやればできると思うのですが、これもあまり美味しくなさそうで、たぶん、似たようなこういうパターンって結構ほかにもありそう。
- ダイアログをあげて、その結果をうけて何か処理
- 何か処理して、その結果を受けて画面遷移
- アニメーションさせて、完了後に何か処理
などなど。
ってことで、 Action の後に Action を呼び出すような形にしました。
で、ここでひねってみたのが ShowDialogAction が発行する ActionCompleted イベント。たぶん Behavior 系をいろいろ作りそうな気がしたので、 INotifyActionCompleted ってインターフェイスを切って、それを ActionCompletedEventTrigger から読むようにしました。
public interface INotifyActionCompleted { event EventHandler<ActionCompletedEventArgs> ActionCompleted; }
public class ActionCompletedTrigger : TriggerBase<DependencyObject> { protected override void OnAttached() { base.OnAttached(); if(!(this.AssociatedObject is INotifyActionCompleted)){ return; } INotifyActionCompleted target = this.AssociatedObject as INotifyActionCompleted; target.ActionCompleted -= AssociatedObject_ActionCompleted; target.ActionCompleted += AssociatedObject_ActionCompleted; } protected override void OnDetaching() { base.OnDetaching(); if (!(this.AssociatedObject is INotifyActionCompleted)) { return; } INotifyActionCompleted target = this.AssociatedObject as INotifyActionCompleted; target.ActionCompleted -= AssociatedObject_ActionCompleted; } private void AssociatedObject_ActionCompleted(object sender, ActionCompletedEventArgs e) { this.InvokeActions(e); } }
ActionCompletedイベントを発行する ShowDialogAction は以下のような感じ。
protected override void Invoke(object parameter) { if (!this.IsExecutableType()) { return; } ChildWindow window = Activator.CreateInstance(this.DialogType) as ChildWindow; if (window == null) { return; } bool isDialogBase = window is DialogBase; window.Closed += (s, e) => { if (this.ActionCompleted != null) { if (!window.DialogResult.HasValue || !window.DialogResult.Value) { return; } object param = isDialogBase ? ((DialogBase)window).ResultObject : null; this.ActionCompleted(this, new ActionCompletedEventArgs(param)); } }; if (isDialogBase) { ((DialogBase)window).Show(this.Parameter); } else { window.Show(); } }
実際の XAML は以下のような感じ。
<HyperlinkButton Content="新しいアカウントを作成"> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <localInteractivity:ShowDialogAction DialogType="{Binding Dialogs.DialogViews.CreateNewAccountDialog, Source={StaticResource DialogWrapper}}"> <i:Interaction.Triggers> <localInteractivity:ActionCompletedTrigger> <localInteractivity:InvokeMethodAction TargetObject="{Binding DataContext, ElementName=Self}" MethodName="AddedNewAccount"> <localInteractivity:InvokeParameter PropertyName="Result" /> </localInteractivity:InvokeMethodAction> </localInteractivity:ActionCompletedTrigger> </i:Interaction.Triggers> </localInteractivity:ShowDialogAction> </i:EventTrigger> </i:Interaction.Triggers> </HyperlinkButton>
こうしといてあげることで、呼び出されたダイアログも、呼び出されたあとのメソッドもかなりすっきりしました。オブジェクトの取り回しとかもあまり意識せず、ダイアログはダイアログの結果を自身のプロパティにセットするだけ、呼び出された後のメソッドも引数を利用するだけ、それぞれの間は 各 Trigger や Action の設定で取り持つ形です。
これ作りながら、条件判定とかも付け加えられそうと思ったのですが、たぶんそれやると Action/Trigger の構造が複雑になりすぎてわけわからんようになりそうなのでいったん保留にしました。
※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/56058
開発記その 9 - T4 で ViewModel を生成 -
ViewModelのプロパティを定義するのがいちいちめんどくさかったので、T4 で生成するようなのを書いてみた。
T4 の先頭で以下のような感じで PropertyDef を複数並べるとそれに対応した C# のコードを生成します。
List<PropertyDef> propertyDefs = new List<PropertyDef>(){ new PropertyDef("UserName",typeof(string)){ Display = new DisplayAttr() {Name = "UserName" , ResourceType = "Assets.Resources.Strings"}, Required = new RequiredAttr() { ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = "Assets.Resources.Strings"}, }, /...
以下のようなコードが生成されます。
private System.String userName_; [Display(Name = "UserName", ResourceType = typeof(Assets.Resources.Strings))] [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(Assets.Resources.Strings))] public System.String UserName { get { return this.userName_; } set { if( this.userName_ != value){ bool cancel = false; System.String oldValue = this.userName_; PreUserNameChange(value, this.userName_, ref cancel); if(cancel){ return; } this.userName_ = value; this.OnPropertyChanged("UserName"); this.ValidateProperty("UserName", this.userName_); PostUserNameChange(value, oldValue); } } } partial void PreUserNameChange(System.String newValue, System.String currentValue, ref bool cancel); partial void PostUserNameChange(System.String newValue, System.String oldValue);
この T4 と、メソッドとかを書くためのパーシャルのファイルをまとめて ItemTemplate にしてみました。
5 行くらい書くと、およそ25行くらいにコードを生成してくれます。約 5 倍。
で、実際のところほんまにこれ嬉しいのかって話なんですが、、、
スニペットで良いんでないの??と、作った張本人がおもってたりします。属性とかは手でガリガリ書かなだめですが、インテリセンスも効いてくれるのでたいした問題でないかと。。むしろ、自由に変更とかできるんでそっちのほうがいいかもとか。。
たぶん、コードで定義してるのがだめなんでしょうね。 DSL Tools とかで頑張って GUI で定義できるようにすれば、中途半端さ加減はなくなると思うのですが、それはそれでやりすぎな気もします。もうちょっとサラッとかけるようなのが欲しいですね。
まぁ、せっかく作ったんで今のところはこれで行きます。
※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/55776
開発記その 8 - T4 で画面遷移用の URL クラスを生成 -
画面遷移で XAML 名をハードコードするのは嫌だったので、プロジェクトの中にある Page を探し出してクラスを作る T4 を書いてみた。生成するクラスは以下のような感じで Pages クラスと PagesWrapper の二つ。
Pages クラスがルートでフォルダはネストクラスで定義している。で親クラスにそのネストクラスのインスタンスを公開するプロパティを持っています。各 Page の XAML は定数フィールドの文字列と URI 型プロパティ、あと引数をつけらるようにメソッドが作成されます。
PagesWrapper は Pages のインスタンスの保持と XAML のリソースで定義できるように用意してます。
コードで呼び出すときは以下のような感じ。
コードで書くときに、インテリセンスが効いてくれるので楽ちんです。ただし、 UriMapper を定義して Cool URI とかってのはできないので、 戻るボタンとか使わないとか、ベタベタの URL がみえてても問題ないって場合だけですね。ちなみに今回は JournalOwnership を OwnsJournal にしてるのでブラウザの戻るボタンは使えないので問題なしです。
※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/55717
開発記その 7 - バインディングエラーがある場合は処理をしないように -
さて、http://d.hatena.ne.jp/k_maru/20100815/1281885406 の続きです。
バインディングエラーがある場合に処理をしないようにするのも同時に実装してました。
前の投稿で実装していた ValidationErrorInfo に IsPropertyValidationError っていうプロパティを実装しています。こいつはプロパティのバインド時に発生したエラーかどうかを保持しています。で、ViewModelBase で保持している一連の ValidationErrorInfo を調べて IsPropertyValidationError が true のものがあるかを取得する HasPropertyValidationErrors ってのを作りました。
public bool HasPropertyValidationErrors { get { if (!this.HasErrors) { return false; } foreach (string key in this.errors.Keys) { ValidationErrorInfo info = this.errors[key].FirstOrDefault(i => i.IsPropertyValidationError); if (info != null) { return true; } } return false; } }
このプロパティをログインボタンクリック時の EventTrigger に仕掛けた ConditionBehavior の条件にして、 true の場合は Action を実行しないようにしています。
<i:EventTrigger EventName="Click"> <i:Interaction.Behaviors> <ei:ConditionBehavior> <ei:ConditionalExpression> <ei:ComparisonCondition LeftOperand="{Binding DataContext.HasPropertyValidationErrors, ElementName=Self}" RightOperand="false" /> </ei:ConditionalExpression> </ei:ConditionBehavior> </i:Interaction.Behaviors> <localInteractivity:InvokeMethodAction Target="{Binding DataContext, ElementName=Self}" MethodName="Login"/> </i:EventTrigger>
この ConditionBehavior って Blend の SDK にシレッと入ってたんやけど、意外と使えそうです。っていうか、 Trigger とかに Behaviour をさせたんですね。具体例はすぐに思い浮かばへんけど、いろいろ使えそうです。
さてさて、やっとこれでログインは完了かな。
※今回のチェックイン(前回と一緒)
http://moneybook.codeplex.com/SourceControl/changeset/changes/54103
開発記その 6 - 複数のプロパティに対応するエラーをグループ化
さて、http://d.hatena.ne.jp/k_maru/20100809/1281355604 で書いたようにバリデーションが微妙すぎたので、少し対応した。
まずは、ログインエラー時のマークを、新しい値が入力されたときに消す対応。
ViewModelBase ではエラーを Dictionary
private class ValidationErrorInfo { public ValidationErrorInfo(string propertyName, string message, bool isPropertyValidationError, string[] groupProperties) { this.PropertyName = propertyName; this.Message = message; this.IsPropertyValidationError = isPropertyValidationError; this.GroupProperties = groupProperties; } public string PropertyName { get; private set; } public string Message { get; private set; } public bool IsPropertyValidationError { get; private set; } public string[] GroupProperties { get; private set; } public override string ToString() { return this.Message; } }
ポイントはコンストラクタの最後の引数の groupProperties か。 AddError メソッドで自前でエラーが設定されたときに、設定するプロパティが複数指定されていたらそれをグループとみなして、保存しておく。
protected void AddError(string message, params string[] propertyNames) { if (propertyNames == null || propertyNames.Length == 0) { return; } foreach (string propertyName in propertyNames) { List<ValidationErrorInfo> messages = GetErrorsList(propertyName); if (messages.FirstOrDefault(m => m.Message == message) == null) { messages.Add(new ValidationErrorInfo(propertyName, message, false, propertyNames)); this.OnErrorChanged(propertyName); } } }
で、プロパティのバリデーション時にグリグリ回してグループに同じメッセージがあったら消すって流れです。
これで、たとえばログインエラーが出ている状態(ユーザー名とパスワードのテキストボックス両方にマークされている状態)で、ユーザー名を変更するとパスワードの方のエラー表示も削除されます。
ちょっとコードがグリグリとループ書いててもっさいので、どこかで修正しないとと思ってるんですが、いいアイデアが浮かばないので保留。
ValidationSummary は相変わらず 2 行表示されてますが、こいつも保留ですね。最終的に使わない方向に高確率でなりそうですが・・・。
※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/54103
開発記その 5 - ログイン画面開発中でバリデーションが微妙すぎる件について -
さて、遅々として進まない開発だが、やっとこさログイン画面の実装が"ほぼ"完了。"ほぼ"と書いたのはバリデーションが非常に微妙すぎるのだ。どこが微妙かと言うと、ログイン実行の時にユーザーがいないかパスワードが間違っていた場合。
現状では、UserName と Password のそれぞれに自前でメッセージを設定して ErrorsChanged を投げている。そのため、以下のような画面が表示されることになる。
ユーザー名とパスワードのそれぞれのテキストボックスが赤くマークされるのは微妙な気もしないではないが、まだ許容範囲内だろう。しかし、 ValidationSummary に 2 行表示されるのは痛い。その他にもいろいろあり、今のところ思っているところを書き出すと以下のような感じである。
- ValidationSummary に 2 行表示されるのはいただけない
- 同じメッセージの場合は 1 行でお願いしたい
- このエラーが表示されているときにユーザー名を変更したら、ユーザー名のマークは消えるがパスワードのマークが残る
- パスワードのマークも消したい
- ボタンが押されたときに (INotifyDataErrorInfo インターフェイスの) HasErrors プロパティが true だったら、有無を言わさず処理をさせたくない
- 現状はなにもチェックせずに各プロパティのバリデーションを実行している。プロパティのバリデーションチェックで引っかかったエラー以外は削除する動きになっているため、暗黙的に自身で設定したエラーは削除されるのだ
ValidationSummary は少し置いておいて、下の二つはコードの方で対応できる気がする。エラーを保存するときにプロパティのバリデーションか設定されたかを保持しておく。設定するエラーの時は、対応するプロパティをグルーピングできるようにしておく。ってところか。ちょっと込入り始めそうな気がするが多分許容範囲内で実装できそう。
ValidationSummary は、、、、あとで考えよう。(自前で使いやすいのを作る結論にしそうな気がしないでもない)
まだまだ、ログイン画面は続きます。
※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/53744