2010年8月2日月曜日

番外編:パフォーマンス

バージョンβ0.04公開しました。
http://wtwitter.codeplex.com/
若干のUIの改善とパフォーマンス改善です。
ちなみにソースコードはなんかUPLOADに失敗したのでまだあげていません。

久しぶりの更新ですが、
この間にパフォーマンスについていろんなサイトで調べたことと若干の経験からわかったことを元に
メモ程度に箇条書きしたいと思います。
私自身ではパフォーマンス計測とかの検証はしていないのであしからず。

・WPFの描画は、アニメーションや透過、エフェクトなどを多用しなければそんなに遅くない(※1)
・パフォーマンスのネックになりやすいのはレイアウト
・今のクライアントの作りでは、ListBoxに『可変サイズの』ListBoxItemをたくさんおいているのが一番の原因だったと思われる。
 高さ固定ならば見えない部分のサイズ計測は不要なので仮想化の恩恵にあずかれるが、
 可変だと実際に配置して計算してみないとわからないので遅い。
・レイアウトがネストすると指数関数的?に計算量が増えるので、
逆にレイアウトを簡素化できれば、表示量はあまり減らさなくても大きく軽量化できる。

※1
WPFはGPU(グラフィックボード)を描画に使うので、
それなりに新しいGPUを積んでいれば描画は早い。
ただし、GPUを使うようなソフト(たとえばWindowモードにした3Dゲームなど)を表示していたら
描画は遅くなる。

2010年7月5日月曜日

番外編:MVVMパターンとDMVVMパターン

※今日は小難しい話で今後に関係ないのでスキップしていただいてもOKです。

世の中ではWPFといったらMVVM(Model-View-ViewModel)パターンという認識ができつつあり、
MVVMにどれだけ厳密に従うか、といった話題を目にすることが多くなったような気がします。

・ViewModelからViewへの依存関係は持たない
・ViewはXamlで書いて、コードビハインドにはコードを持たない
・ViewModelはViewを意識した作りにしない

この連載ではMVVMパターンを使っていますが、
そういう意味ではかなりぬるいMVVMパターンです。

厳密に従った方が、特に大規模な多人数開発において
コードの理解性や保守性が向上しますが、
往々にして生産性(コードを書く時間)との両立は難しい場合が多いです。

Modelへの単純なアクセスのためのプロパティを
ViewModelに大量に追加することは、MVVMで作ったら誰しも経験があるかとは思います。
ある意味、単純であってもコード量の増加は理解性の低下といえます。
また、MVVMの厳密性を守るためのコネクタが肥大化・複雑化したら本末転倒です。

ViewModelがUI(View)を意識するかどうかについても(注:依存するかどうかではないです)難しい問題であり、
完全にUIを意識しないViewModelを書けば、Viewは自由にカスタマイズできるようになりますが、
その分Viewで作り込まないといけない部分が増えることが予想されます。
ある程度はUIを意識した作りにしておいて、Viewはデータを単純に表示するだけという状況に近づければ、
Viewにバグが入り込む余地が減り、ViewModelまではUnitTestできるというMVVMの利点を活かせます。
場合によって一長一短ということです。

これらはプロジェクトの特性と与えられた時間などのトレードオフで検討していただきたいです。

また、私がWPFを始めた時は、よいコードとしてminiUMLが挙げられていて、
そのプロジェクトではMVVMではなくDM-V-VM(DataModel-View-ViewModel)が使われています。
これはMVVMに近いのですが、
ViewからViewModelとDataModel両方のアクセスを許し、
また、ViewModelだけではなくDataModelもWPFにべったりの作りとなっており、
その分シンプルで冗長なコードが少なくなっています。
まぁ作図ツールなのでデータとしては単なるXMLで、
Windowsのクライアント上にどう図形を表示するかがキモなので、
当然の判断のような気もします。

私感ですが、データが単純でUIが複雑なアプリケーションはDM-V-VMが、
逆にデータが複雑でUIが単純なアプリケーションはM-V-VMがあうのではないのでしょうか。
もちろん私が知らないだけで、もっといいパターンもあるかもしれません。
また、どれかを選んで厳密に従わなければならない、ということではなく、
そのパターンのいいところを理解して、うまくあわないところは柔軟にしたほうがいいと思うのです。

といわけで、この連載のコードも悪いところもいいところも(?)あると思いますので、
悪いところはまねしないで、工夫して使ってください!<結局のところ今日はこれが言いたかった。
なんか、ネットで「あのソフトのソースコードはこうなっていたので・・・」という発言をちらほら見て
気になったので書いてみました。


DM-V-VMパターンが気になった方はMiniUMLで検索してソースコードを見てみてください。

あと、更新すると言って更新してなくてすみません。
新しいクライアントのイメージができあがらないのです。。

2010年6月10日木曜日

番外編 VisualStateManagerを使ってみる

今回はVisual State Manager(以下VSM)を使ってみます。
VSMは名前の通り、コントロールなどの表示物の状態管理をしやすくするものです。


詳しい経緯は知らないのですが、
もともとはTriggerが貧弱なSilverLight用に用意されたものらしく、
WPFでは必ずしも使わなくても済ませられるようです。
しかしこれを使うことにより、状態の管理、およびモデルとビューの分離がしやすくなるのではと思っています。


これは.Net Framework3.5だけでは使えないので、
3.5にWPF tool kitを導入するか、.Net Frameworkの4.0を導入してください。


今回のサンプルは以下のようなイメージです。

まずは単純に2つの楕円が回転するだけのコードを。


<Window x:Class="VisualStateManager.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">
    <Grid>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="45" CenterX="50" CenterY="25" x:Name="_rotate1"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="0" CenterX="50" CenterY="25" x:Name="_rotate2"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  <Grid.Triggers>
   <EventTrigger RoutedEvent="Loaded">
    <BeginStoryboard>
     <Storyboard BeginTime="0:0:0" RepeatBehavior="Forever">
      <DoubleAnimation Storyboard.TargetName="_rotate1" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="45" To="405"/>
      <DoubleAnimation Storyboard.TargetName="_rotate2" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="360" To="0"/>
     </Storyboard>
    </BeginStoryboard>
   </EventTrigger>
  </Grid.Triggers>
  
  <Grid.Background>
   <RadialGradientBrush Center="0.5, 0.5">
    <GradientStop Color="Gray" Offset="0"/>
    <GradientStop Color="Black" Offset="1"/>
   </RadialGradientBrush>
  </Grid.Background>
 </Grid>
</Window>

2つの楕円(Ellipse)を配置して、
それぞれRotateTransformで角度を付けています。

あとはLoadedイベントでアニメーション(Storyboard)を開始します。
アニメーションはそれぞれ逆方向に
RotateTransformの角度の数値(double値)を2秒で360度変化するように設定しています。
最後のBackgroundは雰囲気を出すためだけのものです。

ではVisualStateManagerを使ってみます。
状態が「通信中」の時だけアニメーションするようにしてみましょう。
「通信中」かどうかの状態変化はチェックボックスで代用します。


<Window x:Class="VisualStateManagerSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">

 <VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
   <VisualState x:Name="Idle">

   </VisualState>
   <VisualState Name="Accessing">
    <Storyboard BeginTime="0:0:0" RepeatBehavior="Forever">
     <DoubleAnimation Storyboard.TargetName="_rotate1" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="45" To="405"/>
     <DoubleAnimation Storyboard.TargetName="_rotate2" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="360" To="0"/>
    </Storyboard>
   </VisualState>
  </VisualStateGroup>
 </VisualStateManager.VisualStateGroups>

 <Grid>
  <CheckBox Foreground="White" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked">通信中</CheckBox>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="45" CenterX="50" CenterY="25" x:Name="_rotate1"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="0" CenterX="50" CenterY="25" x:Name="_rotate2"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  
  <Grid.Background>
   <RadialGradientBrush Center="0.5, 0.5">
    <GradientStop Color="Gray" Offset="0"/>
    <GradientStop Color="Black" Offset="1"/>
   </RadialGradientBrush>
  </Grid.Background>
 </Grid>
</Window>


Windowの下にを置きます。
ちなみにのような書き方になっていないのは添付プロパティだからです。
んでそこにAccessingとIdleの2つの状態を作っています。
AccessingのVisualStateに前出のStoryboardをごっそり持って行きます。
これでAccessingになったらStoryboardが動き出すようになります。

あとはコードビハインドで状態を変化させてやるだけです。


  private void CheckBox_Checked(object sender, RoutedEventArgs e) {
   VisualStateManager.GoToElementState(this, "Accessing", false);
  }

  private void CheckBox_Unchecked(object sender, RoutedEventArgs e) {
   VisualStateManager.GoToElementState(this, "Idle", false);
  }


VisualStateManagerはWindowが(添付プロパティとして)持っているので
最初の引数はthisです。

さて、GoToElementStateの最初の引数がControl(=View)なので、
ModelとViewModelがViewを知らないという前提のMVVMモデルでは、
ModelやViewModelから単純にこの操作ができません。

これに対するよい解法が以下のページで紹介されています。
http://tdanemar.wordpress.com/2009/11/15/using-the-visualstatemanager-with-the-model-view-viewmodel-pattern-in-wpf-or-silverlight/

というわけで、まずはViewModelを作ります。
これは今までの連載で何度も書いている形なのでわかると思います。


namespace VisualStateManagerSample {
 class CommunicationViewModel : INotifyPropertyChanged {
  public CommunicationViewModel() {
  }

  private bool _isAccessing;
  public bool IsAccessing {
   get {
    return _isAccessing;
   }
   set {
    if (_isAccessing != value) {
     _isAccessing = value;
     OnPropertyChanged("IsAccessing");
     OnPropertyChanged("State");
    }
   }
  }

  public string State {
   get {
    if (IsAccessing) {
     return "Accessing";
    } else {
     return "Idle";
    }
   }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  private void OnPropertyChanged(string propertyName) {
   var handler = PropertyChanged;
   if (handler != null) {
    handler(this, new PropertyChangedEventArgs(propertyName));
   }
  }
 }
}

いつものOnPropertyChangedと、

状態をboolで持っていて、状態を表す文字列Stateが連動しているだけです。

MainWindowのコードビハインドはCheckboxのイベントハンドラが無くなって
以下のようにスッカラカンになります。
上記URLからもらってきたStateManagerをほぼそのままおいています。
※これは別ファイルに書くのがめんどくさかっただけでMainWindowにある必要はありません
唯一書き換えているのはGoToState→GoToElementStateに呼び出しをかえました。
MSDNを見る限りVisualStateManagerをControlTemplateに置くかどうかで使い分けるみたいです。


namespace VisualStateManagerSample {
 /// <summary>
 /// MainWindow.xaml の相互作用ロジック
 /// </summary>
 public partial class MainWindow : Window {
  public MainWindow() {
   InitializeComponent();
  }
 }

 public class StateManager : DependencyObject {
  public static string GetVisualStateProperty(DependencyObject obj) {
   return (string)obj.GetValue(VisualStatePropertyProperty);
  }

  public static void SetVisualStateProperty(DependencyObject obj, string value) {
   obj.SetValue(VisualStatePropertyProperty, value);
  }

  public static readonly DependencyProperty VisualStatePropertyProperty =
   DependencyProperty.RegisterAttached(
   "VisualStateProperty",
   typeof(string),
   typeof(StateManager),
   new PropertyMetadata((s, e) => {
    var propertyName = (string)e.NewValue;
    var ctrl = s as Control;
    if (ctrl == null)
     throw new InvalidOperationException("This attached property only supports types derived from Control.");
    System.Windows.VisualStateManager.GoToElementState(ctrl, (string)e.NewValue, true);
   }));
 }

}


XAMLは以下のようになります。


<Window x:Class="VisualStateManagerSample.MainWindow"
  xmlns:local="clr-namespace:VisualStateManagerSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300"
  local:StateManager.VisualStateProperty="{Binding Path=State}">
 <Window.DataContext>
  <local:CommunicationViewModel/>
 </Window.DataContext>
 
 <VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
   <VisualState x:Name="Idle">

   </VisualState>
   <VisualState Name="Accessing">
    <Storyboard BeginTime="0:0:0" RepeatBehavior="Forever">
     <DoubleAnimation Storyboard.TargetName="_rotate1" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="45" To="405"/>
     <DoubleAnimation Storyboard.TargetName="_rotate2" Storyboard.TargetProperty="Angle"
       Duration="0:0:2" From="360" To="0"/>
    </Storyboard>
   </VisualState>
  </VisualStateGroup>
 </VisualStateManager.VisualStateGroups>

 <Grid>
  <CheckBox Foreground="White" IsChecked="{Binding Path=IsAccessing}">通信中</CheckBox>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="45" CenterX="50" CenterY="25" x:Name="_rotate1"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  <Ellipse Stroke="Green" StrokeThickness="2" Width="100" Height="50">
   <Ellipse.RenderTransform>
    <RotateTransform Angle="0" CenterX="50" CenterY="25" x:Name="_rotate2"/>
   </Ellipse.RenderTransform>
  </Ellipse>
  
  <Grid.Background>
   <RadialGradientBrush Center="0.5, 0.5">
    <GradientStop Color="Gray" Offset="0"/>
    <GradientStop Color="Black" Offset="1"/>
   </RadialGradientBrush>
  </Grid.Background>
 </Grid>
</Window>


DataContextにはViewModelを入れていて、
local:StateManager.VisualStateProperty="{Binding Path=State}"で
ViewModelのStateに応じてVisualStateManagerが動作するようになります。

ViewModelの動作をシミュレートするためにCheckboxを残していますが、
基本的にViewModelが自分の状態を表す文字列を書き換えるだけでよく、
Viewはその文字列に従って状態を表すアニメーションするだけでよい構造になっていることがわかるかと思います。

今回のソースコードはどこにもアップロードしていませんが、
ここに書いている部分で、デフォルトのAppクラスを除いたらすべてです。

2010年5月31日月曜日

番外編 IsSynchronizedWithCurrentItemのメモ

前にどこかにちょっと書いたような気がするけど、メモ。

リストのIsSynchronizedWithCurrentItemは便利でわりと重要だと思うんだけど、
MSDNもエッセンシャルWPFも、現在性管理とか難しい言葉で説明してあるので、
ここではかみ砕いた表現での説明に挑戦します。

個人的には、このプロパティを使うときは2つのパターンのどちらかです。

  1. リストで現在選択中のアイテムを『即時に』取得したい
  2. リストの選択項目と、他の表示コントロールを同期させたい

1は、IsSynchronizedWithCurrentItem=falseの時は
リストからフォーカスが移動したときしかSelectedItemが更新されませんが、
trueにしておくことで、リストの選択項目を変更した瞬間に更新されます。

多くの場合、OKボタンなどを押したときに選択されている項目を取得できればいいのですが、
ショートカットキーとかを駆使したりして、
リストにフォーカスがあるままで細かく選択項目を制御/取得したいときに
trueにすると便利になります。

2はエッセンシャルWPFでも紹介されている、
リストを表示して、リストの選択項目が変わると、
別の領域に選択項目に応じてその項目の詳細を表示するものです。
Master-Detailパターンと呼ばれるものです。

たとえばこんな感じです。
リストボックスにタイムラインの一覧を選択肢としていれておいて、
選択中のタイムラインの説明を下の領域に表示しています。

正直いくつかのサンプルを見ても、どうしてそうなるのかがわからなかったのですが、
たぶん以下のような原理で動いています。
「たぶん」というのが情けないですが。

まず、Window(またはUserControl)のDataContextを
そのままリストのデータとしてバインドしている場合は簡単です。
以下、実際にためしていないのですが、動くはずです。


<UserControl x:Class="Cassador.Twitter.View.Parts.EditDataSource"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
 <DockPanel>
  <ComboBox ItemsSource="{Binding}" DisplayMemberPath="DisplayName"
     IsSynchronizedWithCurrentItem="True" DockPanel.Dock="Top"/>
  <GroupBox Header="説明">
   <TextBlock Text="{Binding Path=Description}" TextWrapping="Wrap"/>
  </GroupBox>
 </DockPanel>
</UserControl>

選択中のアイテムのDescriptionプロパティをTextBlockに表示します。

ですが、MVVMで開発しているとWindowのDataContextはViewModelで
ViewModelのプロパティにリストを持っていて、それにバインドする場合が多いと思います。
ですが、以下のようにすると、動きません(これもためしていませんが、きっと。)


<UserControl x:Class="Cassador.Twitter.View.Parts.EditDataSource"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
 <DockPanel>
  <ComboBox ItemsSource="{Binding Path=DataSourceTemplates}" DisplayMemberPath="DisplayName"
     IsSynchronizedWithCurrentItem="True" DockPanel.Dock="Top"/>
  <GroupBox Header="説明">
   <TextBlock Text="{Binding Path=Description}" TextWrapping="Wrap"/>
  </GroupBox>
 </DockPanel>
</UserControl>

理由は、TextBlockが見ているのはWindowのDataContext(のしたのDescription)であり
リストが操作しているのは前の例と違ってWindowのDataContextではないからです。

これを解決する方法はいくつかあると思いますが、
1つはTextBlockのDataContextをComboBoxとあわせてやることです。
以下実際に使っているコード。



<UserControl x:Class="Cassador.Twitter.View.Parts.EditDataSource"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" >
 <DockPanel>
  <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
   <Button Command="{Binding Path=CloseCommand}">_Cancel</Button>
   <Button>_OK</Button>
  </StackPanel>
  <DockPanel DataContext="{Binding Path=DataSourceTemplates}">
   <ComboBox ItemsSource="{Binding}" DisplayMemberPath="DisplayName"
       IsSynchronizedWithCurrentItem="True" DockPanel.Dock="Top" Name="listbox"/>
   <GroupBox Header="説明">
    <TextBlock Text="{Binding Path=Description}" TextWrapping="Wrap"/>
   </GroupBox>
  </DockPanel>
 </DockPanel>
</UserControl>




DataContextをあわせるために、DockPanelを1枚かませています
(DataContextを持たせられれば他のコントロールでもいいと思います)
上記の例の場合、ボタンのCommandのはリストの選択項目とは無関係の
DataContextのCloseCommandにバインディングしていることに注目してください。
ボタンを内側のDockPanelの中に持ってくると、
それはリストの選択項目のインスタンスのCloseCommandにバインドされるということになります。
(そして選択項目にCloseCommandがなければもちろん動きません)

2010年5月30日日曜日

番外編 MEFを使ってみる

MEFは、プラグインなどのあとから拡張できる構造を
簡単に実現することができる仕組みだと認識しています。
以前はプラグインを実行時に読み込むには、
リフレクションを使ってそれなりのコードを書かないといけなかったのですが、
MEFによりだいぶ単純になっていて、また柔軟さを持っているようです。
※使いこなしていないので断言できません

本当はこれまでのように実際のコードで例を示したかったのですが、
複雑になりそうなのでサンプルコードで示します。

まず一つのクラス内でむりやり書いたコードです。

一応twitterクライアントで使えそうなシチュエーションということで、
タイムラインを表すITimelineインターフェイスと、
その具体的な実装PublicTimelineクラスとFriendTimelineクラスがあります。

そしてExport属性を付けたクラスを
ImportまたはImportMany属性を付けたプロパティが受け取ります。

コンストラクタでやっていることは、
CatalogクラスがExportを探してくる役目、
CompositionContainerがExportとImportの参照関係を構築する役目、
といったところです。(たぶん)



using System.ComponentModel.Composition;
using System.Reflection;
using System.ComponentModel.Composition.Hosting;


 public partial class MainWindow : Window {
  public MainWindow() {
   DataContext = this;
   var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
   var container = new CompositionContainer(catalog);
   container.ComposeParts(this);
   InitializeComponent();
  }

  public interface ITimeline {
   string Name { get; }
  }

  [Export(typeof(ITimeline))]
  class PublicTimeline : ITimeline {
   public string Name {
    get { return "みんなのタイムライン"; }
   }
  }

  [Export(typeof(ITimeline))]
  class FriendTimeline : ITimeline {
   public string Name {
    get { return "友達のタイムライン"; }
   }
  }

  [ImportMany]
  public IEnumerable<ITimeline> AllTimelines {
   get;
   set;
  }
 }

もちろん実際Exportするのははインナークラスである必要はありません。

ためしにViewを以下のように書いたらタイムライン名が表示されるはずです。


<Window x:Class="MEFSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
  <ListBox ItemsSource="{Binding Path=AllTimelines}" DisplayMemberPath="Name"/>
 </Grid>
</Window>

使う側(Importする側)でITimelineの実装クラスを列挙する必要がないことに注目してください。
また、タイムラインの実装クラスを増やしたい場合はExportがついたクラスを増やすだけです。
この場合も使う側(Importする側)に変更がいりません。

ただこの方法では、同じアセンブリからしかImportできません。
他のファイル(.dll)からImportするには、DirectoryCatalogを使います。
上のサンプルとは全く関係ありませんが、
作成中のアプリのコードで示すと、
Pluginディレクトリのなかのすべてのdllファイルの中の
IServiceインターフェイスの実装を探すコードは
たとえば以下のような感じになります。


   using (var dirCatalog = new DirectoryCatalog("Plugins"))
   using (var container = new CompositionContainer(dirCatalog)) {
    _services = container.GetExportedValues<IService>();
   }



(1例目はMSDNのサンプルがそうなっていたのでやっていませんでしたが、
Dispose()があるので実際のコードの2例目はusingで囲っています。)

これで、「プラグインはIServiceを実装してPluginディレクトリに入れる」というルールを決めるだけで
アプリケーション側はプラグインの一覧を簡単に探すことができます。
実用にはもっといろいろと決めることがありますが、
基本はこんなところです。

他にもCatalogの種類はあるし、いろんな呼び出し方がありそうなので、
詳しくは仕様を調べてください。
クラスではなくてメソッドをインポートする、などできます。
インポートに条件を付けたりもできます。
たくさんページがありますが、公式では以下のページが比較的わかりやすいかなと思いました。
http://msdn.microsoft.com/ja-jp/magazine/ee291628.aspx

2010年5月25日火曜日

番外編 拡張メソッドを使ってみる

ちょっと新しく作り直しているところがいろいろ躓いているので、
しばらく小ネタでつないでみたいと思います。
トピック作ってまで書くほどことではないかもしれませんが、
他に書くことがないので。。

今日使ってみるのは『拡張メソッド』。
既存のクラスにメソッドを追加できます。
といっても、クラスの内部(privateメンバとか)にアクセスできないので、
クラスにメソッドが『追加されたかのように見せる』機能のイメージですね。

この機能は以下の2つの理由であまり多用しない方がいいような気がします。
・本来のクラスと別の場所にコードがあるので、探しにくくなる
・本来持っていないメソッドが存在するようになるので、
 この機能に慣れてない人が、拡張メソッドが使われているところを読むと『???』となる可能性がある


ですが使い方によっては、ちょっとした手間でコードを読みやすくできそうです。


たとえば、以下のようなコードがあったとします。


if (string.IsNullOrEmpty(filename)) {


ここで以下のような拡張メソッドを作ると


 public static class StringExtension {
  public static bool IsNullOrEmpty(this string text) {
   return string.IsNullOrEmpty(text);
  }

以下のように書けます。


if (filename.IsNullOrEmpty()) {


後者の方が英語としてすんなり読めますよね?

このように、stringのような自分では手を加えられないクラスに、
拡張メソッドによってメソッドを追加することにより、
読みやすいコードを書くことができるようになります。

まぁ人によってはあんまり効果があると思わないかもしれませんが、
ロジックが複雑になってif分の中がごちゃごちゃしてくるほど
この若干の読みやすさの差がだいぶ効いてくるような気がします。

今後の記事で時々出てくるかもしれないので紹介してみました。

2010年5月17日月曜日

番外編 Todoアプリを作ってみた2

http://minitodo.codeplex.com/

ついったークライアントが行き詰まっているので、Todoアプリを更新してみました。
今回はアニメーションの説明でも。
ソースが必要な場合は上のリンクから取ってね。

最初に断っておきますが、MVVMとアニメーションは相性が悪いらしいです。
http://blog.sharplab.net/computer/cprograming/wpf/3065/
の記事とか読んだ限り。
そして今回の私の投稿はMVVMを思いっきり無視して作っています。
むしろ邪道なやり方と言ってもいいかもしれません。
ちゃんとやりたい方は上記のリンク先のやり方とかやってください。

そして本流のついったークライアントではないので、かなり省いて説明します。
実際の動きがイメージできないときはバイナリをDLしてためしてください。

まず、Todoを追加したときに、
右からスライドインしてくるようなアニメーションを作ります。

XAML


       <!--新規作成時のスライドインアニメーション用-->
       <Grid.RenderTransform>
        <TranslateTransform x:Name="_slideInTransform"></TranslateTransform>
       </Grid.RenderTransform>
       <Grid.Resources>
        <!--新規アイテムを挿入するアニメーション-->
        <Storyboard x:Key="_slideInAnimation">
         <DoubleAnimation Storyboard.TargetName="_slideInTransform"
                 Storyboard.TargetProperty="X"
                 From="{Binding ElementName=_window, Path=Width}" Duration="0:0:0.5">
         </DoubleAnimation>
        </Storyboard>

Gridは1つのアイテムを表す入れ物です。(ListBoxItemの直下)
GridにTranslateTransformをつけておいて
(プロパティを付けていないので、おいただけでは何もしない)、
そのTranslateTransformのXの値を5秒間でWidthから0に減らすStoryboardを
Resourceに入れておきます。



  private void Grid_Loaded(object sender, RoutedEventArgs e) {
   var container = sender as Grid;
   var vm = container.Tag as TodoViewModel;//ViewModelを渡す簡単な方法が思いつかないので、Tagに入れている
   var id = vm.Id;
   var animation = container.FindResource("_slideInAnimation") as Storyboard;

   if (!_alreadyAnimated.Contains(id)) {
    animation.Begin();
    _alreadyAnimated.Add(id);
   }
  }

んで、Loadedイベントでアニメーションを開始します。
Loadされた時には2番目以降のアイテムが1行ずつ下がっていますので、
新しいアイテムが本来の位置からXだけ右に表示される(そしてXは徐々に減っていく)というわけです。
ただし、それだけだとすべてのアイテムがLoadされるたびにアニメーションされるので、
新規ではないやつはアニメーションしないようにIDを管理しておきます。
これだけです。
Loadedでやるのがスマートではないですね。

次に完了したらフェードアウトする処理です。


        <Storyboard x:Key="_doneAnimation">
         <DoubleAnimation Storyboard.TargetName="_itemContainer"
              Storyboard.TargetProperty="Opacity"
              To="0" BeginTime="0:0:0.5" Duration="0:0:1"/>
         <DoubleAnimation Storyboard.TargetName="_itemBackground"
              Storyboard.TargetProperty="Opacity"
              From="1" Duration="0:0:0.1"/>

背景(WhiteだけどOpacity=0.01→ほぼ透明)を一瞬だけ白くする(Opacity=1)アニメーションと
アイテム自体を徐々に透明にするアニメーションを入れています。

これもMVVM的にVMのコマンドを直接バインドすると
アニメーションをするまえにListBoxから無くなってしまうので、
Viewのコードビハインドのイベントハンドラで


   var animation = container.FindResource("_doneAnimation") as Storyboard;
   animation.Completed += (s, eArg) => {
    vm.CompleteCommand.Execute(null);
   };
   animation.Begin();

というように、StoryboardのCompletedイベントで
ViewModelの完了コマンドを呼ぶようにしています。
(アニメーションしている間はViewModel的には完了していないということです。)