泥庭

2015年4月11日

[WPF]描画パフォーマンスのお話(Polylineの場合)

Filed under: C#, performance, WPF — タグ: , — yone64 @ 11:06 PM
WPFの描画速度には、いつも悩ませてもらってます。
最近、Polylineで描画している際にちょっと不思議(?)な現象に出会ったので、一応メモ程度に。

(more…)

2015年4月6日

【WPF】カスタムメッセージボックス

Filed under: C#, WPF — タグ: , , , — yone64 @ 11:56 PM
WPF標準のメッセージボックスは、何かと機能不足でカスタマイズ性が低くて困ってしまいますね。
そこで、通常のWindowをMessageBoxっぽく見せることを検討してみたいと思います。

なお、Windows7以降のみをターゲットとできる方は、Windows API Code PackのTaskDialogをお勧めします。
良記事→ http://grabacr.net/archives/tag/windows-api-code-pack
# いまさらXP対応はないよね。Windows Server 2003ももうすぐサポート切れるし…
# Embedded とか、知らない子ですし。
というわけで、誰得なエントリーです。

(more…)

2015年3月10日

【ReactiveProperty】きちんとDisposeしよう

Filed under: .NET, C#, WPF — タグ: , , , — yone64 @ 3:05 PM
ReactivePropertyが依存しているReactiveExtensionsのお話なのですが、きちんとにDisposeしないと予期しない動作の原因になります。

下記サンプル
MainWindow.xaml
<Window x:Class="WpfApplication3.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>
        <Viewbox>
            <TextBlock Text="{Binding DateTime.Value}" />
        </Viewbox>
    </Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApplication3
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private IDisposable _disposable;

        public MainWindow()
        {
            InitializeComponent();

            var vm = new MainViewModel();
            this.DataContext = vm;

            this._disposable = vm as IDisposable;
            this.Closing += (s, e) =>
            {
                if (this._disposable != null) this._disposable.Dispose();
            };
        }
    }
}
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Reactive.Bindings;
 
namespace WpfApplication3
{
    public class MainViewModel : IDisposable
    {
        public ReactiveProperty<string> DateTime { get; private set; }
 
        public MainViewModel()
        {
            this.DateTime = 
                Observable.Interval(TimeSpan.FromMilliseconds(10), NewThreadScheduler.Default)
                          .Select(_ => System.DateTime.Now.ToString("HH:mm:ss.fff")).ToReactiveProperty();
        }
 
        void IDisposable.Dispose()
        {
            this.DateTime.Dispose();
        }
    }
}
タイマーで時間を表示するだけの簡単なサンプルですが、ReactivePropertyをDisposeしないと終了しないアプリケーションになってしまいます。
この場合は、NewThreadSchedulerによって新しいThreadが立っているので当然の結果なのですが、Rxを使うと簡単にThreadを起こせるので、神経質にDisposeしていたほうが良い気がしました。

というわけで、基底クラスとして次のようなものを用意するようにしています。
ViewModelBase.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Text;
using System.Threading.Tasks;

namespace WpfApplication3
{
    public abstract class ViewModelBase : IDisposable
    {
        protected CompositeDisposable Disposable { get; private set; }

        public ViewModelBase()
        {
            this.Disposable = new CompositeDisposable();
        }

        void IDisposable.Dispose()
        {
            this.Disposable.Dispose();
        }
    }
}
CompositeDisposableは、Rxが提供しているIDisposableをまとめるためのクラスです。
これを利用すると、MainViewModelが次のように少し簡易になります。
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Reactive.Bindings;
 
namespace WpfApplication3
{
    public class MainViewModel : ViewModelBase
    {
        public ReactiveProperty<string> DateTime { get; private set; }
 
        public MainViewModel()
        {
            this.DateTime = 
                Observable.Interval(TimeSpan.FromMilliseconds(10), NewThreadScheduler.Default)
                          .Select(_ => System.DateTime.Now.ToString("HH:mm:ss.fff"))
                          .ToReactiveProperty()
                          .AddTo(this.Disposable); // かずきさん指摘により修正

            // this.Disposable.Add(this.DateTime);
        }
    }
}
ToReactiveProperty()とDisposable.Add()をまとめてうまく書けるといいんですけどね。↓のような、拡張メソッドを増やすのしか思いつきませんでした。
public static class ViewModelBaseEx
{
    public static ReactiveProperty<T> ToReactiveProperty<T>(this IObservable<T> souce, CompositeDisposable disposable)
    {
        var rp = souce.ToReactiveProperty();
        disposable.Add(rp);
        return rp;
    }
}

2015年3月9日

Thicknessの125000倍の長さのGeometryをOnRenderで描画

Filed under: .NET, C#, WPF — タグ: , — yone64 @ 11:15 PM
前回の続きっぽいもの。

CustomControl等でOnRenderをOverrideして描画することが多々あると思いますが(ありますよね!?)
その際は、少し回避方法が異なります。(というか、かなり謎です)
protected override void OnRender(DrawingContext drawingContext)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(new Point(0, 100), false, false);
        context.LineTo(new Point(125000, 100), true, false);
    }

    drawingContext.DrawGeometry(null, new Pen(Brushes.Black, 1), geometry);

    base.OnRender(drawingContext);
}
まず、描画できるパターン。125000倍なので、描画可能です。
protected override void OnRender(DrawingContext drawingContext)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(new Point(0, 100), false, false);
        context.LineTo(new Point(125000.1, 100), true, false);
    }

    drawingContext.DrawGeometry(null, new Pen(Brushes.Black, 1), geometry);

    base.OnRender(drawingContext);
}
125000倍を超えると描画不可能です。(Lineと同じ)
protected override void OnRender(DrawingContext drawingContext)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(new Point(0, 100), false, false);
        context.LineTo(new Point(125000.1, 100.1), true, false);
    }

    drawingContext.DrawGeometry(null, new Pen(Brushes.Black, 1), geometry);

    base.OnRender(drawingContext);
}
ところが、傾けても描画できるようになりません。
protected override void OnRender(DrawingContext drawingContext)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(new Point(0, 100), false, false);
        context.LineTo(new Point(0.1, 100), true, false);
    }

    var geometry2 = new StreamGeometry();
    using (var context = geometry2.Open())
    {
        context.BeginFigure(new Point(0, 200), false, false);
        context.LineTo(new Point(375000, 200), true, false);
    }

    drawingContext.DrawGeometry(null, new Pen(Brushes.Transparent, 1), geometry);
    drawingContext.DrawGeometry(null, new Pen(Brushes.Red, 1), geometry2);

    base.OnRender(drawingContext);
}
ちなみに、一つGeometryをDummyで描画しておくと、2つ目のGeometryに対する制限はなくなってるように見えます。(謎謎

Thicknessの125000倍の長さのLine

Filed under: .NET, C#, WPF — タグ: , — yone64 @ 10:47 PM
WPFの制限(?)なのかはわかりませんが、StrokeThicknessの125000倍を超える水平線は引けません。
<Window x:Class="TooLongLine.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>
        <!-- 水平LineはStrokeThicknessの12500倍以下しか描画できない -->

        <!-- 125000倍ちょうどなので描画可能 -->
        <Line X1="0" X2="125000" Y1="5" Y2="5" StrokeThickness="1" Stroke="Red" />
        <!-- 125000倍を超えるので描画不可能 -->
        <Line X1="0" X2="125000.1" Y1="15" Y2="15" StrokeThickness="1" Stroke="Red" />

        <!-- Thicknessを2にすると、125000倍以下になるので描画可能 -->
        <Line X1="0" X2="125000.1" Y1="25" Y2="25" StrokeThickness="2" Stroke="Blue" />
        <!-- 125000倍ちょうどなので描画可能 -->
        <Line X1="0" X2="250000" Y1="35" Y2="35" StrokeThickness="2" Stroke="Blue" />
        <!-- 125000倍を超えるので描画不可能 -->
        <Line X1="0" X2="250000.1" Y1="45" Y2="45" StrokeThickness="1" Stroke="Blue" />

        <!-- 水平でなければ描画可能 -->
        <Line X1="0" X2="250000.1" Y1="55" Y2="55.001" StrokeThickness="1" Stroke="Green" />
        
        <!-- Polylineで、一つの線分が125000倍以下でも水平になってしまうとダメ -->
        <Polyline Points="0,65 100000,65 200000,65" StrokeThickness="1" Stroke="Orange" />
    </Grid>
</Window>
上記、XAMLを実行すると下記Windowが表示されます。

キャプチャ

どうしてもこの制限に引っかかる場合は、微妙に斜めの線にしてみればよいのかもしれません。
その前に、本当にそんなに長いLineが必要かは要検討ですねw

(参考)Horizontal or vertical WPF Lines limited to 125,000 pixels?
http://stackoverflow.com/questions/13731593/horizontal-or-vertical-wpf-lines-limited-to-125-000-pixels

2015年2月26日

Gridからの脱出

Filed under: C#, WPF — タグ: , , , — yone64 @ 12:38 AM
WPFではPanelにControlを配置することになるわけですが、PanelからControlがはみ出しても表示することができます。

例えば、
<Window x:Class="WpfApplication2.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">
    <Canvas Background="Ivory" Margin="50">
        <TextBlock Text="abcdefghi" FontSize="100"/>
    </Canvas>
</Window>
という、XAMLだと

キャプチャ

という、実行結果になります。Canvasの背景を「Ivory」にしてるので、文字がCanvas領域をはみ出しているのがわかると思います。
ちなみに、ClipToBoundというプロパティーが用意されていて、これをTrueに設定することで、Panelの大きさの外にはみ出した部分が切り取られます。
<Window x:Class="WpfApplication2.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">
    <Canvas Background="Ivory" Margin="50" ClipToBounds="True">
        <TextBlock Text="abcdefghi" FontSize="100"/>
    </Canvas>
</Window>

キャプチャ

ところが、GridではPanelをはみ出して表示することができません。明示的にClipToBoundをFalseにしても、ダメでした。
<Window x:Class="WpfApplication2.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 Background="Ivory" Margin="50" ClipToBounds="False">
        <TextBlock Text="abcdefghi" FontSize="100"/>
    </Grid>
</Window>

キャプチャ

CanvasのClipToBound=”True”の場合と、同じ表示結果になります。
# 決して同じ画像ではありませんw

そういう場合は、Gridの中にCanvasを配置してください。Canvas内部に配置されたControlが親Gridの範囲をも超えて描画されるようになります、
<Window x:Class="WpfApplication2.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 Background="Ivory" Margin="50">
        <Canvas Background="AliceBlue">
            <TextBlock Text="abcdefghi" FontSize="100"/>
        </Canvas>
    </Grid>
</Window>

キャプチャ

2015年1月14日

WPFでBingMapを使う

Filed under: .NET, C#, WPF — タグ: , , — yone64 @ 2:29 AM
今回からFont大き目でお届けしたいと思います。少し見やすくなるとよいのですが?

WPFで地図表示がしたい

地図表示といえば、やっぱりGoogleMapですかね。でも、WPF-C#からならBingMapが相性よさそうですよね。(ほら、なんとなくMicrosoft同士だし。)
と思って探しているとやっぱりありました。
Bing Maps WPF Control
しかし、いまどきmsiでインストールしなければいけないとかちょっと残念感が漂う感じ。(NuGetからインストールできるようになるとよいですね。)
というわけで、msiを落としてきてインストールすれば準備完了です。(あ、あとBingMapのAPI Keyも取得しておいてください)

地図表示アプリの作成

インストールが終了したら、さっそくプロジェクトを作成してみましょう。
# なお、ここからの作業はすべて、VisualStudio2013 & Windows8.1で行ってます。
  • 新規プロジェクト作成→参照設定追加
C:\Program Files (x86)\Bing Maps WPF Control\V1\Libraries にインストールされた、Microsoft.Maps.MapControl.WPF.dll への参照を追加します。
キャプチャ

  • コントロール追加
MainWindow.xamlにMapコントロールを追加します。

<Window x:Class="BingMapSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpf="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <wpf:Map/>
    </Grid>
</Window>
すると、デザインビューで地図が確認できます。
キャプチャ

  • 実行
ここで、一度実行してみましょう。
キャプチャ

地図が表示されましたね。「Invalid Credentials.~」というのはAPI Keyを入力してないがための文字列なのでとりあえずはOKです。

  • API Key設定
では、API Keyを設定して再び実行してみましょう

<wpf:Map CredentialsProvider="ここにAPI Keyを記述"/>
さて、再度実行!
キャプチャ

あれ?地図が表示されない!?

BingMapとの戦い(1回戦)

API Keyを設定したら、地図が表示されなくなってしまいました。仕方がないので、API Keyがない状態に戻して再度実行をしてみます。
やっぱり、地図は表示されません。なんだか不吉な感じがしてきました。ここで試したことを並べていきます。

  • リビルド → ×
  • ソリューションのクリーン → ×
  • Visual Studio再起動 → ×
  • Windows再起動 → ×
OSの再起動までしてダメなら、割ともう打つ手はない感じです。
仕方がないので、別のマシンを取り出して同じことを試してみます。そうこうしているうちに、ある共通点が見つかりました。

OSの1Userにつき、BingMapを表示するアプリケーションは一度目だけ地図が表示される。
これは、VisualStudioからの実行だけでなく、EXEを直接起動した場合も同じである
ということは、ここで一つの仮説が思いつきますね!

BingMapのDLLは、初回実行時にどこかにキャッシュ(?)を作成しており、キャッシュがある場合のみ地図表示に失敗する。(たぶんキャッシュロードのバグか何かで)
比較的善意に解釈してみました。(悪意を持って2回目以降の地図表示を拒否してるとどうしようもないけど、Microsoftのコントロールはそういうことしないよねという前提)
.NETのモジュールがキャッシュファイルを作りそうな箇所はそれほど多くないので探してみます。思いつくのは↓あたりでしょうか

  • C:\ProgramData
  • C:\Users\{各ユーザ}\AppData
で、結局下記にありました。
キャプチャ

IsolatedStorage(分離ストレージ)に保存されていたようです。
【参考】(@IT)分離ストレージを活用するには?[C#、VB]
ファイルを削除することにより、2回目以降の起動時にも地図が表示されることが確認されました。

対策検討会議

というわけで原因を見つけたものの、勝利!とはならないのです。
分離ストレージのファイルを削除すれば地図が表示されるとわかったものの、起動時に乱数で作ったっぽいフォルダ名の中にあるファイルを見つけて削除するというのは、まぁありえないわけです。
ならどうするか?

その前に、ひとつ疑問が。ヒントは2つ。

  • 2回目以降表示されないバグがあれば、さすがにネット検索にヒットしそう
  • 分離ストレージのファイル名末尾に「_JA-JP」とついている
ここから、導き出される答えは?
ひょっとして、日本語環境だけ?
日本語(+α)環境のみの不具合であれば、StackOverflowが引っかかることもないし、まぁありえる話かなと。
これは、英語OSでの実行を試してみるしかないですよね。でも英語OSなんて手元にない、なんてときは、Azureが便利ですよ。
で、結果はBingo。英語OSでは起きていなかったのです。

ここまでくれば、少し光明が見えてきた感じがします。
.NETのDLLが実行時のCultureを判断して何かしてるということは
  • Thread.CurrentThread.CurrentUICulture
  • Thread.CurrentThread.CurrentCulture
のどちらかが絡んでいるはず。(というか、英語OSを試す前に気付きたかったです。)
で、両方とも試した結果、CurrentUICultureが関連していることが判明。アプリケーションの起動時にCultureを変更するコードを入れた結果、無事地図が表示されるようになったのでした。
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
    }
}
めでたし、めでたし。というわけにはいきませんね。CurrentUICultureがen-US固定になってしまいます。
さらに試行錯誤を加えた結果、次のことが判明しました。

Mapコントロールをアプリケーションで最初に初期化する時にen-USだったらよい

つまり、いったんCultureをen-USにして、Mapコントロールを初期化したのち、再びCultureを元に戻せばよさそうです。
結局、↓のようなコードを起動時に仕込むことになりました。

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        var old = Thread.CurrentThread.CurrentUICulture;
        Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
        var dummy = new Map();
        Thread.CurrentThread.CurrentUICulture = old;
    }
}

余談

設定するCultureは、en-US以外にもen-GBでもよいようです。その他のCultureもいくつか試してみましたが、ダメでした。
この2つのCultureに共通する点として、一つ気付いたのは、距離の単位です。

キャプチャ

milesになっているのがわかると思います。ちなみに、公式にヤード・ポンド法が使われている国は、英国と米国だけだそうです。
ひょっとするとこのことが関係しているのかもしれません。(裏は取ってないので単なる想像ですが。)

後半へ続く

大きな落とし穴が開いていたBingMapControlですが、ひとまず表示できるようになりました。
ただ、戦いがこれで終わったわけではありません。というわけで、続きは次回。さらに大きな落とし穴が待ち受けています。(死

2014年12月1日

Room metro #28 でしゃべってきました。

Filed under: .NET, WPF, 勉強会, 未分類 — タグ: , , , — yone64 @ 11:12 PM

先日行われた、Room metro #28 「XAML Day」で、しゃべってきました。

ネタかぶりしなさそうなWF(Window Workflow Foundation)がらみのはずなのに、まさかのネタかぶりとかありましたが、実際はWPFのコードしか書いてないのでかぶってないはず(

資料は、↓で公開しています。

また、デモに使ったコードは↓です。

https://github.com/yone64/Metro28

2014年7月27日

ReactivePropertyを使いたい人のための、ReactiveExtensions入門(その4)

Filed under: .NET, LINQ, WPF — タグ: , , , , — yone64 @ 11:28 AM

以前のバージョンは→第1回第2回第3回

今回は、PropertyChanged周りを見てみます。Rx分少なめで…。
ReactivePropertyはMVVM周りで使われるため、INotifyPropertyChangedとは切っても切れない関係があります。

var m = new Model();

var io = Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
    h => (s, e) => h(e),
    h => m.PropertyChanged += h,
    h => m.PropertyChanged -= h);

 this.Name = io.Where(e => e.PropertyName == "Name")
    .Select(e => m.Name)
    .ToReactiveProperty(m.Name);

上記のような、PropertyChangedイベントをIObservableに変換して、ToReactivePropertyするコードは何度も書くことになります。
そのため、Utilityメソッドとして定義しておくとよいでしょう。

public static IObservable<PropertyChangedEventArgs> PropertyChangedAsObservable<T>(this T source) where T : INotifyPropertyChanged
{
    return Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
        h => (s, e) => h(e),
        h => source.PropertyChanged -= h,
        h => source.PropertyChanged += h);
}

とりあえず、Event→IO<T>変換拡張メソッドは作りますよね。で、次はPropertyNameでフィルタリングするのですが、文字列で指定するのは、いろいろと嫌なのでExpressionを引数にとれるようにします。

public static IObservable<PropertyChangedEventArgs> PropertyChangedAsObservable<TObj, TProp>(this TObj source, Expression<Func<TObj, TProp>> expression)
    where TObj : INotifyPropertyChanged
{
    var name = ((MemberExpression)expression.Body).Member.Name;

    return Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
        h => (s, e) => h(e),
        h => source.PropertyChanged -= h,
        h => source.PropertyChanged += h)
        .Where(e => e.PropertyName == name);
}

PropertyChangedにおいてはEvetArgsが帰ってきてもうれしいことは少ないので、実際に変更された値を返却するように変更します。引数にデリゲートを追加してもよいのですが、せっかくなので既に引数に追加されているExpressionを利用したいと思います。
Expression<Func<TObj, TProp>>からFunc<TObj,TProp>を作るには、ReflectionとExpression.Compileがありますが、ここは処理速度的に有利なReflectionを使いたいと思います。(dynamicがこの目的では使えないのが残念ですね)

// .net4 まではこちら 
var func = (Func<TObj, TProp>)Delegate.CreateDelegate(typeof(Func<TObj, TProp>), typeof(TObj).GetProperty(name).GetGetMethod());

// PCL だとこっち
var func2 = (Func<TObj, TProp>)typeof(TObj).GetTypeInfo().GetRuntimeProperty(name).GetMethod.CreateDelegate(typeof(Func<TObj, TProp>));

Reflection周りは、ストアアプリ・PCL関連で大きく手が入っているので、.NET4までとPCLとでは書き方が大きく異なります。(.NET4.5だとどっちでもOK)

public static IObservable<TProp> PropertyChangedAsObservable<TObj, TProp>(this TObj source, Expression<Func<TObj, TProp>> expression)
    where TObj : INotifyPropertyChanged
{
    var name = ((MemberExpression)expression.Body).Member.Name;
    var func = (Func<TObj, TProp>)Delegate.CreateDelegate(typeof(Func<TObj, TProp>), typeof(TObj).GetProperty(name).GetGetMethod());

    return Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
        h => (s, e) => h(e),
        h => source.PropertyChanged -= h,
        h => source.PropertyChanged += h)
        .Where(e => e.PropertyName == name)
        .Select(e => func(source));
}

CreateDelegateはそこそこ重い処理なので、実際に使うときはキャッシュしておきましょう。
あとは、ToReactivePropertyの引数で与えていた初期値をRx側で対応します。

public static IObservable<TProp> PropertyChangedAsObservable<TObj, TProp>(this TObj source, Expression<Func<TObj, TProp>> expression)
    where TObj : INotifyPropertyChanged
{
    var name = ((MemberExpression)expression.Body).Member.Name;
    var func = (Func<TObj, TProp>)Delegate.CreateDelegate(typeof(Func<TObj, TProp>), typeof(TObj).GetProperty(name).GetGetMethod());

    return Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
        h => (s, e) => h(e),
        h => source.PropertyChanged -= h,
        h => source.PropertyChanged += h)
        .Where(e => e.PropertyName == name)
        .Select(e => func(source))
        .StartWith(func(source));
}

こういうのを一つ作っておくと、ReactivePropertyの活用がはかどりますね。
とここまで書いて、ReactivePropertyのDLLにも同じようなメソッドがあることに気付きました…

INotifyPropertyChangedExtensions.ObserveProperty

Observable.Deferメソッドで囲んであったり少し違いますが、ほぼほぼ同じですよね。(きっと
# この場合、Deferで囲むとどう動作が変わるか分かってない

2014年7月1日

ReactivePropertyを使いたい人のための、ReactiveExtensions入門(その3)

Filed under: .NET, LINQ, WPF — タグ: , , , — yone64 @ 10:21 PM

以前のバージョンは → 第1回第2回
サブタイトルをつけておけばよかったなと思いつつ、適当なサブタイトルが思いつかないので今回もそのまま行きます。

さて、MVVM方式(?)でアプリケーションを作成していると、ViewModelが親子構造になることはよくあります。
で、その場合に親ViewModelが子ViewModelのProperty変更を監視したいということも少なからずあります。
ちなみに、非ReactivePropertyな世界だと、PropertyのSetterにイベントの着脱を仕込むのが一般的なのではないでしょうか(と勝手に思ってます)。

private SubContentViewModel _subContent;
public SubContentViewModel SubContent
{
    get
    {
        return _subContent;
    }
    set
    {
        if (_subContent == value) return;
        if (_subContent != null) _subContent.PropertyChanged -= PropertyChanged;
        _subContent = value;
        if (_subContent != null) _subContent.PropertyChanged += PropertyChanged;
    }
}

これを、ReactivePropertyを利用して実現してみます。何も考えないと以下のようにSelectManyで簡単に実現できるように思えます。
# SubContentとTextはそれぞれReactivePropertyです。

this.SubContent
    .Where(s => s != null)
    .SelectMany(s => s.Text)
    .Subscribe(t => Console.WriteLine(t));

しかし、これだと一度購読を開始したSubContentがいつまでたっても解放されないため、メモリリークなどの原因になってしまいます。
これを以下のように記述することで、親ViewModelから参照されなくなった子ViewModelの変更購読を解放することができるようになります。

this.SubContent
    .Select(s => s == null ? null : s.Text.Subscribe(t => Console.WriteLine(t)))
    .Scan(new Tuple<IDisposable, IDisposable>(null, null), (a, b) => Tuple.Create(a.Item2, b))
    .Subscribe(t => { if (t.Item1 != null) t.Item1.Dispose(); });

長いな…

Older Posts »

WordPress.com で無料サイトやブログを作成.