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的には完了していないということです。)

2010年5月10日月曜日

お知らせ

近況ですが、なんか今の作りに不満がでてきたので、作り直しを考えています
不満なのは

  • UnitTestがやりづらい構造になっている
  • twitterにべっとりの作りになっている
  • パフォーマンスが悪い
あとは、単純に.net framework 4.0の機能をがんがん使ってみたかったり。

もちろん今の構造を徐々に修正していくのもありですが、
今ならまだ作り直した方がいいかなぁ、、と。
もちろん流用できるところはそのまま持ってくるので、
作り直しでも、そこまで今までのが無駄になるとは思っていません。

ブログの再開はあと1週間後くらいからかなぁ。。と。