開発記その 4 - Domain Service の引数にエンティティ -

やっとユーザー作成の完成です。サボりすぎ。。

で、表題の件ではまりました。

ユーザー作成は Silverlight の BusinessApplication のテンプレートについてるのですが、そのまま使うのではなく、抜き出す形で MembershipService っていう名前で作りました。

メソッドは CreateUser っていうのをひとつだけ作って、引数は CreateUserData ってのをそのまま使ってます。

[Invoke(HasSideEffects = true)]
public CreateUserStatus CreateUser(CreateUserData user, string password) 

で、コンパイルするとコンパイルエラー。

エラー	1	Operation named 'CreateUser' does not conform to the required signature. Parameter types must be an entity type or one of the predefined serializable types.	MoneyBook

とりあえず、MSDN に直行してみたのですが Invoke オペレーションの引数は何でも OK って書いてあります。

http://msdn.microsoft.com/en-us/library/ee707373%28v=VS.91%29.aspx

仕方ないので、ネットで 1 時間くらいうろついたのですがめぼしい情報がなくって BusinessApplicatoin のテンプレートに戻ったら発見しました。

どうやら、現状では戻り値で戻ってる型じゃないとダメっぽいです。

/// <summary>
/// Query method that exposes the <see cref="RegistrationData"/> class to Silverlight client code.
/// </summary>
/// <remarks>
/// This query method is not used and will throw <see cref="NotSupportedException"/> if called.
/// Its primary job is to indicate the <see cref="RegistrationData"/> class should be made
/// available to the Silverlight client.
/// </remarks>
/// <returns>Not applicable.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
public IEnumerable<RegistrationData> GetUsers()

で、以下のようなメソッドを追加したら無事にコンパイルが通りました。

[Query(IsComposable = false)]
public CreateUserData GetCreateUserData() {
    throw new NotSupportedException();
}


さて、これでやっとユーザー作成は完了しました。


※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/53069

開発記その 3 - HTMLのようにデフォルタボタンを設定したいなら Behavior で -

なかなかユーザー作成にたどり着けないでいてますが・・・。

画面を立ち上げて動作確認をしてると、テキストボックスに値を入力して、キーボードから手をはなしてマウスを持ってボタンをクリックするのが非常にめんどくさくなってきます。HTML の Submit ならそのままエンターキーでクリックできのに。。

そこで、、、

作りました。

public class DefaultButtonBehavior : Behavior<FrameworkElement> {

  public ButtonBase TargetButton {
    get { return (ButtonBase)GetValue(TargetButtonProperty); }
    set { SetValue(TargetButtonProperty, value); }
  }

  public static readonly DependencyProperty TargetButtonProperty =
    DependencyProperty.Register("TargetButton", typeof(ButtonBase), typeof(DefaultButtonBehavior), null);

  private void AssociatedObject_KeyDown(object sender, KeyEventArgs e) {
    if (e.Key != Key.Enter || this.TargetButton == null) {
      return;
    }
    AutomationPeer peer = ButtonBaseAutomationPeer.CreatePeerForElement(this.TargetButton);
    peer.SetFocus();

    Dispatcher.BeginInvoke(() => {
      ((IInvokeProvider)peer.GetPattern(PatternInterface.Invoke)).Invoke();
    });
  }

  protected override void OnAttached() {
    base.OnAttached();
    this.AssociatedObject.KeyDown += AssociatedObject_KeyDown;
  }

  protected override void OnDetaching() {
    base.OnDetaching();
    this.AssociatedObject.KeyDown -= AssociatedObject_KeyDown;
  }
}

設定方法は以下のような感じ。

<Grid Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Top">
  <i:Interaction.Behaviors>
    <localInteractivity:DefaultButtonBehavior TargetButton="{Binding ElementName=CreateUserButton}" />
  </i:Interaction.Behaviors>

  <Button Content="{Binding CreateUserButton,Source={StaticResource Strings}}" 
      Grid.Row="3" Grid.ColumnSpan="2" HorizontalAlignment="Right" 
      Width="90" Height="25" x:Name="CreateUserButton">
    <i:Interaction.Triggers>
      <i:EventTrigger EventName="Click">
        <localInteractivity:InvokeMethodAction 
          Target="{Binding DataContext, ElementName=Self}" MethodName="CreateUser"/>
      </i:EventTrigger>
    </i:Interaction.Triggers>
  </Button>

</Grid>

ポイントは、Invoke する前にフォーカスをボタンに移して Dispatcher で一呼吸おいてるところでしょうか。どうも VM のプロパティにバインディングされるのがロストフォーカスした後っぽい(未検証)ので、こういう動きにしました。これがないと、 VM のプロパティに値が設定されずにクリックイベントを受けたメソッドが動いてしまいました。


すべてのコントロールやタイミングでこのパターンが有効かどうかは未検証ですが、とりあえずは OK ってことで。


※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/51734

開発記その 2 - バリデーション -

さてさて、 Silverlight では自前で頑張ったり、バインディング時に例外飛ばしたり、IDataErrorInfo を使って通知したりとかバリデーションとその表示方法がいくつかありますが、個人的には INotifyDataErrorInfo がいいかなと思ってます。何故かって言うと・・・

  • いつでも好きなタイミングでバリデーションエラーを通知できる
  • 同じ項目に複数のメッセージ(バリデーションエラー)を設定できる

とこですかね。
その他のバインディングを前提にしたのは、バインディング時しかエラーを出せないですからね。

ってことで、以下のような実装を。

private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();

private string userName;
[Display(Name = "UserName", ResourceType = typeof(Assets.Resources.Strings))]
[Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(Assets.Resources.Strings))]
public string UserName {
  get { return this.userName; }
  set {
    if (this.userName != value) {
      this.userName = value;
      OnPropertyChanged("UserName");
      ValidateProperty("UserName", this.userName);
    }
  }
}

protected void ValidateProperty(string propertyName, object newValue) {
  List<ValidationResult> results = new List<ValidationResult>();
  Validator.TryValidateProperty(newValue, new ValidationContext(this, null, null) { MemberName = propertyName }, results);

  bool isChanged = false;

  if (results.Count == 0) {
    if (this.errors.ContainsKey(propertyName)) {
      this.errors.Remove(propertyName);
      isChanged = true;
    }
  } else {
    if (!this.errors.ContainsKey(propertyName)) {
      this.errors.Add(propertyName, new List<string>());
    }

    List<string> currentErrors = this.errors[propertyName];

    foreach (ValidationResult result in results) {
      if (!currentErrors.Contains(result.ErrorMessage)) {
        currentErrors.Add(result.ErrorMessage);
        isChanged = true;
      }
    }

    foreach (string currentError in currentErrors.ToArray()) {
      if (results.FirstOrDefault(result => result.ErrorMessage == currentError) == null) {
        currentErrors.Remove(currentError);
        isChanged = true;
      }
    }
  }

  if (isChanged) {
    OnErrorChanged(propertyName);
  }
}

で、実装するときに考えたのが、バリデーションをかけるタイミングです。上のコードではプロパティに値を設定しきって、変更通知も出したあとにバリデーションをかけてます。パッと見は気持ち悪いんですけどね。

なぜ、こうしたかってのは、値を設定し切る前にバリデーションかけてダメだったら値を設定しないようにすると画面上の見た目(テキストボックスとかの値)は変更されているのに、VM の値は変更されてない状態になって差異がでるからです。そんな差異が出てる状態で例えばボタンが押されたときにもう一度バリデーションをかけても、画面上とは違う値を利用する形になってしまうんです。タイミングによってはエラーになる値が入力されている可能性があるのにバリデーションが通ってしまうおそれもありますね。

で、この値を設定してからバリデーションをかけるようにすると、ある程度は大丈夫になるんですが、型変換エラーの場合はまだダメなんですね。例えば、int32 型で年齢とかのプロパティの場合、テキストボックスに "あいうえお" とか入力されると、値がそもそもプロパティにセットされない。で、画面上と VM の値に差異が出てしまう。

対処方として、プロパティは全部 String にしてしまうとかもあるんですが、それはそれでイケテない。ってことで、型変換エラーがでる入力ができないコントロールを作るのがベターですかね。このへんはまた作ったときに書こうと思います。

あと、プロパティに対応しないバリデーションとかもどうしようか悩み中。いま、 ValidationSummary を使ってるんですがこいつでそういうのって標準じゃ対応できないっぽいのです。まぁ、こっちも必要になったときに考えてまた書こうと思います。


※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/51612

開発記その 1 - VM のメソッド呼び出し -

MoneyBook の開発に着手しました。

MoneyBook は複数のユーザーで使うことを想定しているので、 ASP.NET の Membership を使ってログイン画面かなと思ったのですが、そもそもユーザー登録する画面も欲しくなったので、まずは登録画面から作りはじめました。テキストボックスが並んでボタンを押したらユーザーが作成される普通の画面です。


ボタンを押したら VM のメソッドを呼んで処理を行うわけですが、 WPF とかだったら Command を使うところですが実はあんまり好きじゃない。わざわざ Command にデリゲートを設定して、公開するっていう手間がかかるし、気持ち的には一発で VM のメソッドを呼んで欲しいわけです。しかも Silverlight の Command は今のところボタンのクリックにしか対応してないので、いまいち。


と、いうことで他の手段を考えるわけですが、実は Expression BlendSDK に CallMethodAction ってのがあります。こいつは任意のオブジェクトのメソッドを呼び出してくれる Action です。以下のような感じ。

こいつの TargetObject に Binding を使って、ルート要素なりなんなりの DataContext を指定すれば、その DataContext に設定されている VM のメソッドが呼ばれるってわけです。

{Binding ElementName=LayoutRoot,Path=DataContext}

Blend で設定できるし、VM のメソッドを一発で呼び出してくれるのでいい感じですね。さらにボタンのクリックだけじゃなくって Trigger の内容次第で呼び出したいタイミングを自由に設定できるので、例えば ListBox の SelectionChanged なんかでも OK です。

が、今回はちょっと思うところあって車輪の再発明になりますが、同じ機能のものを別に作りました。思うところってのはまた後日。それが以下の InvokeMethodAction です。ただ単純に指定されたメソッドをリフレクションで探し出して呼び出してるだけです。

[DefaultTrigger(typeof(UIElement), typeof(System.Windows.Interactivity.EventTrigger), "MouseLeftButtonDown")]
[DefaultTrigger(typeof(ButtonBase), typeof(System.Windows.Interactivity.EventTrigger), "Click")]
[DefaultTrigger(typeof(Selector), typeof(System.Windows.Interactivity.EventTrigger), "SelectionChanged")]
public class InvokeMethodAction : TriggerAction<FrameworkElement> {

  public InvokeMethodAction() {
  }

  public object Target {
    get { return (object)GetValue(TargetProperty); }
    set { SetValue(TargetProperty, value); }
  }

  public static readonly DependencyProperty TargetProperty =
    DependencyProperty.Register("Target", typeof(object), typeof(InvokeMethodAction), null);

  public string MethodName { get; set; }

  protected override void Invoke(object parameter) {
    if (this.Target == null || string.IsNullOrWhiteSpace(this.MethodName)) {
      return;
    }
    Type targetType = this.Target.GetType();
    MethodInfo method = targetType.GetMethod(this.MethodName, BindingFlags.Public | BindingFlags.Instance);
    if (method == null) {
      throw new InvalidOperationException(String.Format(ErrorMessages.TargetMethodNotFound,this.MethodName));
    }
    method.Invoke(this.Target, null);
  }
}

DefaultTriggerAttribute を指定してあげておくことで、 Blend で Action を落としたときに自動で設定される Trigger を指定することができます。

※今回のチェックイン
http://moneybook.codeplex.com/SourceControl/changeset/changes/51093

開発記その 0 - はじめに -

以前からお金の支出をちゃんと記録しないとなと思ってたのですが、ここ最近大量にお金を使う機会がありまして、またお金を貯める必要が出てきたのでやっと重い腰をあげようかなと思い始めました。

最初はエクセルでいいかなと思ったり、どっかのフリーソフトでいいかなと思ったりもしたのですが、どうもいい感じのものが見当たらなかったので、この際作ろうと思い立ちました。

ってことで、ついでにあまり実は触っていない Silverlight 4 を使っていろいろ試そうかなと。で、その過程をブログで綴ったら完成までちゃんとつくるかなと。と、いうことで「Money Book 開発記」始めます。

さて、MoneyBook を作るに当たって試したいことは以下。

  • CodePlex で公開
  • MVVM で綺麗に実装
  • WCF RIA Service で綺麗に実装
  • そのうち WP 7 アプリも

まずは CodePlex で公開ってことで、以下にプロジェクトサイトを作りました。
http://moneybook.codeplex.com/

ではでは、いつまで続きますことやら。

※最後に記事に対応するチェックインの URL を書くようにしておきます。

http://moneybook.codeplex.com/SourceControl/changeset/changes/50919