プログラミング勉強ノート

プログラミングで勉強した事を書いていきます。

ReactivePropertyでDataGridの列自動生成を試してみた

ReactivePropertyを使用してDataGridの列の自動生成機能を試した時のメモ

  • 列自動生成機能を使用すると、パブリックなプロパティに対応した列が作成される
  • プロパティの値が表示されるが、プロパティがReactivePropertyの場合はValueプロパティを表示するようにしたい
  • AutoGeneratingColumnイベント発生時にReactivePropertyの場合はデータバインドにValueを使用し、Valueのデータ型に応じて列作成するように変更

参考ページ

列作成処理

■ ReactiveProperty用の列作成処理を定義

  • プロパティがReactivePropertyの時に列を作成して返す
  • データバインド時にValueを使用するように変更
  • Valueのデータ型に応じて列を作成する
public static class ReactivePropertyUtility
{
    public static bool TryCreateAutoColumn(string propertyName, Type propertyType, out DataGridColumn dataGridColumn)
    {
        dataGridColumn = null;

        // ReactivePropertyでない場合は処理終了
        if (!(propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Codeplex.Reactive.ReactiveProperty<>)))
            return false;

        // Valueプロパティの情報を取得
        var valuePropertyInfo = propertyType.GetProperty("Value");
        if (valuePropertyInfo == null)
            return false;
        var valuePropertyType = valuePropertyInfo.PropertyType;

        // データ型に応じて列作成する
        DataGridComboBoxColumn comboBoxColumn = null;

        if (valuePropertyType.IsEnum)
        {
            comboBoxColumn = new DataGridComboBoxColumn();
            comboBoxColumn.ItemsSource = Enum.GetValues(valuePropertyType);
            dataGridColumn = comboBoxColumn;
        }
        else if (typeof(string).IsAssignableFrom(valuePropertyType))
        {
            dataGridColumn = new DataGridTextColumn();
        }
        else if (typeof(bool).IsAssignableFrom(valuePropertyType))
        {
            dataGridColumn = new DataGridCheckBoxColumn();
        }
        else if (typeof(Uri).IsAssignableFrom(valuePropertyType))
        {
            dataGridColumn = new DataGridHyperlinkColumn();
        }
        else
        {
            dataGridColumn = new DataGridTextColumn();
        }

        // 並び替え可能か設定
        if (!typeof(IComparable).IsAssignableFrom(valuePropertyType))
        {
            dataGridColumn.CanUserSort = false;
        }

        // ヘッダーの名称
        dataGridColumn.Header = propertyName;

        // データバインドの設定、更新できない場合はOneWayを設定
        DataGridBoundColumn boundColumn = dataGridColumn as DataGridBoundColumn;
        if (boundColumn != null || comboBoxColumn != null)
        {
            Binding binding = new Binding(propertyName + ".Value");
            if (comboBoxColumn != null)
            {
                comboBoxColumn.SelectedItemBinding = binding;
            }
            else
            {
                boundColumn.Binding = binding;
            }

            if (valuePropertyInfo.CanWrite)
                binding.Mode = BindingMode.TwoWay;
            else
                binding.Mode = BindingMode.OneWay;
        }
        return true;
    }
}


■ AutoGeneratingColumnイベント発生時に列を差し替える

// 列自動生成イベント
this.dataGrid1.AutoGeneratingColumn += (sender, args) =>
{
    DataGridColumn column;
    // 列情報が作成できた場合に列を差し替える
    if (ReactivePropertyUtility.TryCreateAutoColumn(args.PropertyName, args.PropertyType, out column))
    {
        // 差し替え
        args.Column = column;
    }
};


コード全体

  • MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using Codeplex.Reactive;

namespace DataGridSample1
{
    public class MainViewModel
    {
        public MainViewModel()
        {
            // テストデータを作成
            this.DataGridData = new[]
            {
                new Record(false, "Name1", 20, GenderType.Male),
                new Record(true, "Name2", 30, GenderType.Female)
            }
            .ToObservable()
            .ToReadOnlyReactiveCollection();
        }

        public ReadOnlyReactiveCollection<Record> DataGridData { get; private set; }
    }

    // 性別
    public enum GenderType { Male, Female }

    // テストレコード用の定義クラス
    public class Record
    {
        public ReactiveProperty<bool> IsCheck { get; private set; }
        public ReactiveProperty<string> Name { get; private set; }
        public ReactiveProperty<int> Age { get; private set; }
        public ReactiveProperty<GenderType> Gender { get; private set; }
        public ReactiveProperty<long> AutoCountUp { get; private set; }

        public Record(bool check, string name, int age, GenderType gender)
        {
            this.IsCheck = new ReactiveProperty<bool>(check);
            this.Name = new ReactiveProperty<string>(name);
            this.Age = new ReactiveProperty<int>(age);
            this.Gender = new ReactiveProperty<GenderType>(gender);
            // 1秒間隔で値を更新
            this.AutoCountUp = Observable.Interval(TimeSpan.FromSeconds(1)).ToReactiveProperty();
        }
    }
}


<Window x:Class="DataGridSample1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:DataGridSample1"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainViewModel></vm:MainViewModel>
    </Window.DataContext>
    
    <Grid>
        <DataGrid Name="dataGrid1" ItemsSource="{Binding DataGridData}"></DataGrid>
    </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 DataGridSample1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // 列自動生成イベント
            this.dataGrid1.AutoGeneratingColumn += (sender, args) =>
            {
                DataGridColumn column;
                // 列情報が作成できた場合に列を差し替える
                if (ReactivePropertyUtility.TryCreateAutoColumn(args.PropertyName, args.PropertyType, out column))
                {
                    // 差し替え
                    args.Column = column;
                }
            };
        }
    }

    public static class ReactivePropertyUtility
    {
        public static bool TryCreateAutoColumn(string propertyName, Type propertyType, out DataGridColumn dataGridColumn)
        {
            dataGridColumn = null;

            // ReactivePropertyでない場合は処理終了
            if (!(propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Codeplex.Reactive.ReactiveProperty<>)))
                return false;

            // Valueプロパティの情報を取得
            var valuePropertyInfo = propertyType.GetProperty("Value");
            if (valuePropertyInfo == null)
                return false;
            var valuePropertyType = valuePropertyInfo.PropertyType;

            // データ型に応じて列作成する
            DataGridComboBoxColumn comboBoxColumn = null;

            if (valuePropertyType.IsEnum)
            {
                comboBoxColumn = new DataGridComboBoxColumn();
                comboBoxColumn.ItemsSource = Enum.GetValues(valuePropertyType);
                dataGridColumn = comboBoxColumn;
            }
            else if (typeof(string).IsAssignableFrom(valuePropertyType))
            {
                dataGridColumn = new DataGridTextColumn();
            }
            else if (typeof(bool).IsAssignableFrom(valuePropertyType))
            {
                dataGridColumn = new DataGridCheckBoxColumn();
            }
            else if (typeof(Uri).IsAssignableFrom(valuePropertyType))
            {
                dataGridColumn = new DataGridHyperlinkColumn();
            }
            else
            {
                dataGridColumn = new DataGridTextColumn();
            }

            // 並び替え可能か設定
            if (!typeof(IComparable).IsAssignableFrom(valuePropertyType))
            {
                dataGridColumn.CanUserSort = false;
            }

            // ヘッダーの名称
            dataGridColumn.Header = propertyName;

            // データバインドの設定、更新できない場合はOneWayを設定
            DataGridBoundColumn boundColumn = dataGridColumn as DataGridBoundColumn;
            if (boundColumn != null || comboBoxColumn != null)
            {
                Binding binding = new Binding(propertyName + ".Value");
                if (comboBoxColumn != null)
                {
                    comboBoxColumn.SelectedItemBinding = binding;
                }
                else
                {
                    boundColumn.Binding = binding;
                }

                if (valuePropertyInfo.CanWrite)
                    binding.Mode = BindingMode.TwoWay;
                else
                    binding.Mode = BindingMode.OneWay;
            }
            return true;
        }
    }
}

ReactivePropertyでDirectoryTreeをバインド

ディレクトリツリーをTreeViewにReactivePropertyを使ってバインドしてみた時のメモ書き。

  • 親ノード展開時に子ノードが1階層分の子情報を取得。
  • 一度展開されたノードは、FileSystemWatcherクラスで変更監視して変更を反映させる。

メモ

コレクションから削除されたタイミングでのDisposeの呼び方を調べる。
→ ObserveRemoveChangedItemsメソッドでコレクションから削除されたタイミングで処理できるみたい。

参考ページ

ディレクトリツリー用のViewModel定義

■ DirectoryTreeViewModelのベースを定義
  • Viewとのバインド用のプロパティ定義
  • 内部処理用のプロパティ定義
public class DirectoryTreeViewModel
{
    /* Viewとのバインド用プロパティ */
    // 名称
    public ReactiveProperty<string> Text { get; private set; }
    // 選択フラグ
    public ReactiveProperty<bool> IsSelected { get; private set; }
    // 展開フラグ
    public ReactiveProperty<bool> IsExpanded { get; private set; }
    // 子要素
    public ReadOnlyReactiveCollection<DirectoryTreeViewModel> Children { get; private set; }

    /* 内部処理用のプロパティ */
    // ディレクトリ
    public DirectoryInfo Directory { get; private set; }
    // 親ノード
    public DirectoryTreeViewModel ParentNode { get; private set; }
    // Dispose用
    public CompositeDisposable Disposable { get; private set; }

    // ルート情報を作成
    public DirectoryTreeViewModel()
    {

    }

    // ノード情報を作成
    public DirectoryTreeViewModel(DirectoryInfo directory, DirectoryTreeViewModel parent)
    {

    }
}


■ ルート情報作成の処理
  • ルートディレクトリ情報取得用のメソッドを定義
  • ルートディレクトリ情報を作成
public static class DirectoryExtensions
{
    /// <summary>
    /// ルート要素のディレクトリ情報を取得
    /// </summary>
    /// <returns></returns>
    public static IEnumerable<DirectoryInfo> GetRootDirectories()
    {
        // とりあえず固定ドライブのみを取得
        return Environment.GetLogicalDrives()
            .Where(x => new DriveInfo(x).DriveType == DriveType.Fixed)
            .Select(x => new DirectoryInfo(x));
    }
}

// ルート情報を作成
public DirectoryTreeViewModel()
{
    this.Text = new ReactiveProperty<string>("ルート");
    this.IsSelected = new ReactiveProperty<bool>(false);
    this.IsExpanded = new ReactiveProperty<bool>(false);
    this.Children = DirectoryExtensions.GetRootDirectories()
        .Select(x => new DirectoryTreeViewModel(x, this))
        .ToObservable()
        .ToReadOnlyReactiveCollection();
}


■ ノード情報作成の処理
  • ノード情報の作成処理
  • とりあえず親要素が展開された初回にだけ子要素を取得するようにする。
// ノード情報を作成
public DirectoryTreeViewModel(DirectoryInfo directory, DirectoryTreeViewModel parent)
{
    this.Directory = directory;
    this.ParentNode = parent;

    this.Text = new ReactiveProperty<string>(this.Directory.Name);
    this.IsSelected = new ReactiveProperty<bool>(false);
    this.IsExpanded = new ReactiveProperty<bool>(false);

    // 子要素のディレクトリ情報取得用のObservable
    // アクセス不可の場合は空情報を作成する
    var childrenObservable = Observable.Defer(() => this.Directory.EnumerateDirectories().ToObservable())
        .Catch((UnauthorizedAccessException ex) => Observable.Empty<DirectoryInfo>());

    // 親要素の展開フラグがtrueになった初回時に子要素を取得
    this.Children = this.ParentNode.IsExpanded.Where(x => x)
        .DistinctUntilChanged()
        .SelectMany(x => childrenObservable)
        .Select(x => new DirectoryTreeViewModel(x, this))
        .ToReadOnlyReactiveCollection();
}


■ FileSystemWatcherクラスのイベント変換処理
  • FileSystemWatcherのイベントをObservable化するクラスを定義
public static class FileSystemWatcherExtensions
{
    /// <summary>
    /// ChangedイベントをObservable化
    /// </summary>
    /// <param name="source"></param>
    /// <returns></returns>
    public static IObservable<FileSystemEventArgs> ChangedAsObservable(this FileSystemWatcher source)
    {
        return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
            h => (sender, args) => h(args),
            h => source.Changed += h,
            h => source.Changed -= h);
    }
    
    /// <summary>
    /// CreatedイベントをObservable化
    /// </summary>
    /// <param name="source"></param>
    /// <returns></returns>
    public static IObservable<FileSystemEventArgs> CreatedAsObservable(this FileSystemWatcher source)
    {
        return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
            h => (sender, args) => h(args),
            h => source.Created += h,
            h => source.Created -= h);
    }
    
    /// <summary>
    /// DeletedイベントをObservable化
    /// </summary>
    /// <param name="source"></param>
    /// <returns></returns>
    public static IObservable<FileSystemEventArgs> DeletedAsObservable(this FileSystemWatcher source)
    {
        return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
            h => (sender, args) => h(args),
            h => source.Deleted += h,
            h => source.Deleted -= h);
    }
    
    /// <summary>
    /// RenamedイベントをObservable化
    /// </summary>
    /// <param name="source"></param>
    /// <returns></returns>
    public static IObservable<RenamedEventArgs> RenamedAsObservable(this FileSystemWatcher source)
    {
        return Observable.FromEvent<RenamedEventHandler, RenamedEventArgs>(
            h => (sender, args) => h(args),
            h => source.Renamed += h,
            h => source.Renamed -= h);
    }
}


■ ディレクトリツリーの変更通知をするObservableクラスを作成
  • ディレクトリツリーの変更通知をするObservableクラス
  • FileSystemWatcherの作成、削除、名前変更のイベントを加工、合成して作成
public class FileSystemWatcherObservable
{
    public enum ChangeEventType
    {
        Create, Delete
    };

    public string Path { get; private set; }
    public ChangeEventType EventType { get; private set; }

    public FileSystemWatcherObservable(string path, ChangeEventType type)
    {
        this.Path = path;
        this.EventType = type;
    }

    public static IObservable<FileSystemWatcherObservable> GetObservable(DirectoryInfo directory)
    {
        return saveDict[directory.Root.FullName];
    }

    // ディレクトリツリー変更監視用のIObservable保存用ディクショナリ
    static Dictionary<string, IObservable<FileSystemWatcherObservable>> saveDict = new Dictionary<string, IObservable<FileSystemWatcherObservable>>();

    // ルートディレクトリ毎に変更監視用のIObservableを作成
    static FileSystemWatcherObservable()
    {
        foreach (var item in DirectoryExtensions.GetRootDirectories())
        {
            saveDict.Add(item.Root.FullName, CreateObservable(item));
        }
    }

    // ディレクトリツリー変更監視用のIObservable作成処理
    static IObservable<FileSystemWatcherObservable> CreateObservable(DirectoryInfo directory)
    {
        var subject = new Subject<FileSystemWatcherObservable>();

        // FileSystemWatcher作成
        var watcher = new FileSystemWatcher();
        // ディレクトリのルートをパスに設定
        watcher.Path = directory.Root.FullName;
        // ディレクトリの変更情報のみを通知
        watcher.NotifyFilter = NotifyFilters.DirectoryName;
        // サブディレクトリ以下も監視する
        watcher.IncludeSubdirectories = true;

        // Createdイベントを加工
        var createEvent = watcher.CreatedAsObservable()
            .Select(x => new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Create));
        // Deletedイベントを加工
        var deleteEvent = watcher.DeletedAsObservable()
            .Select(x => new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Delete));
        // Renamedイベントを加工
        // RenamedイベントはDeleteとCreateの2つに分割する
        var renameEvent = watcher.RenamedAsObservable()
            .SelectMany(x =>
            {
                return new[]
                {
                    new FileSystemWatcherObservable(x.OldFullPath, ChangeEventType.Delete),
                    new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Create)
                }
                .ToObservable();
            })
            .Publish();

        // 加工したFileSystemWatcherをマージして値を発行する
        Observable
            .Merge(
                ThreadPoolScheduler.Instance,
                createEvent,
                deleteEvent,
                renameEvent
            )
            .Subscribe(
                x => subject.OnNext(x),
                ex => subject.OnError(ex),
                () => subject.OnCompleted());

        renameEvent.Connect();

        watcher.EnableRaisingEvents = true;
        return subject.AsObservable();
    }
}


■ ノード情報作成処理にディレクトリツリーの変更通知を追加
  • ディレクトリツリーの変更監視をして、展開後のディレクトリ削除、作成時に変更を反映
  • 要素削除時にIObservableの購読解除する。(削除された子孫要素以下全て)
// 子孫要素を全て取得
public IEnumerable<DirectoryTreeViewModel> GetAllChildrenEnumerable()
{
    Queue<DirectoryTreeViewModel> queue = new Queue<DirectoryTreeViewModel>();
    foreach (var node in this.Children)
        queue.Enqueue(node);

    while(queue.Count>0)
    {
        var node = queue.Dequeue();
        yield return node;
        foreach (var child in node.Children)
            queue.Enqueue(child);
    }
}

// ノード情報を作成
public DirectoryTreeViewModel(DirectoryInfo directory, DirectoryTreeViewModel parent)
{
    this.Disposable = new CompositeDisposable();
    this.Directory = directory;
    this.ParentNode = parent;

    this.Text = new ReactiveProperty<string>(this.Directory.Name);
    this.IsSelected = new ReactiveProperty<bool>(false);
    this.IsExpanded = new ReactiveProperty<bool>(false);

    // 子要素のディレクトリ情報取得用のObservable
    // アクセス不可の場合は空情報を作成する
    var childrenObservable = Observable.Defer(() => this.Directory.EnumerateDirectories().ToObservable())
        .Catch((UnauthorizedAccessException ex) => Observable.Empty<DirectoryInfo>())
        .Select((x, i) => CollectionChanged<DirectoryTreeViewModel>.Add(i, new DirectoryTreeViewModel(x, this)));

    // ディレクトリツリー変更通知を受け取る
    var watcherObservable = FileSystemWatcherObservable.GetObservable(this.Directory);
    // 現ノードの直近の子情報のイベントのみを処理する
    watcherObservable = watcherObservable.Where(x =>
    {
        string parentPath = new string(
            x.Path.Reverse()
                .SkipWhile(y => y != Path.DirectorySeparatorChar)
                .Skip(1)
                .Reverse()
                .ToArray()
            );
        return parentPath == this.Directory.FullName.Trim('\\');
    });

    // Createdイベントでノード追加のCollectionChangedを作成
    var createEvent = watcherObservable.Where(x => x.EventType == FileSystemWatcherObservable.ChangeEventType.Create)
        // 同一名称が存在しないか確認
        .Where(x =>
        {
            return !this.Children.Any(y => y.Text.Value == Path.GetFileName(x.Path));
        })
        .Select(x =>
        {
            var name = Path.GetFileName(x.Path);
            // 文字列比較して挿入位置のノードを取得
            var node = this.Children.FirstOrDefault(y => y.Text.Value.CompareTo(name) > 0);
            // 追加するindex位置を取得
            int index;
            if (node == null)
                index = this.Children.Count;
            else
                index = this.Children.IndexOf(node);
            // 要素追加
            return CollectionChanged<DirectoryTreeViewModel>.Add(index, new DirectoryTreeViewModel(new DirectoryInfo(x.Path), this));
        });

    // Deleteイベントでノード削除のCollectionChangedを作成
    var deleteEvent = watcherObservable.Where(x => x.EventType == FileSystemWatcherObservable.ChangeEventType.Delete)
        // 削除対象が存在するか確認
        .Where(x =>
        {
            return this.Children.Any(y => y.Text.Value == Path.GetFileName(x.Path));
        })
        .Select(x =>
        {
            var name = Path.GetFileName(x.Path);
            // 文字列比較して削除位置のノードを取得
            var node = this.Children.FirstOrDefault(y => y.Text.Value == name);
            // 削除するindex位置を取得
            int index = this.Children.IndexOf(node);
            return CollectionChanged<DirectoryTreeViewModel>.Remove(index);
        });

    // 子要素のコレクション情報更新・削除用のObservableを作成
    var mergeObservable = Observable
        .Merge(
            this.ParentNode.IsExpanded
                .Where(x => x)
                .Take(1)
                .SelectMany(y => Observable.Concat(childrenObservable, createEvent.ObserveOnUIDispatcher())),
            deleteEvent.ObserveOnUIDispatcher()
        )
        .Publish();
    this.Children = mergeObservable.ToReadOnlyReactiveCollection();
    mergeObservable.Connect()
        .AddTo(this.Disposable);

    // 要素削除された時の後処理
    this.Children.ObserveRemoveChangedItems()
        .Subscribe(x =>
        {
            foreach(var deleteNode in x)
            {
                deleteNode.Disposable.Dispose();
                foreach(var child in deleteNode.GetAllChildrenEnumerable().ToArray())
                {
                    child.Disposable.Dispose();
                }
            }
        })
        .AddTo(this.Disposable);
}


コード全体

  • MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Codeplex.Reactive;
using Codeplex.Reactive.Extensions;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;

namespace DirectoryTreeViewSample
{
    public class MainWindowViewModel
    {
        public MainWindowViewModel()
        {
            this.DirectoryTree = new[] { new DirectoryTreeViewModel() };
        }

        public IEnumerable<DirectoryTreeViewModel> DirectoryTree { get; private set; }
    }

    public class DirectoryTreeViewModel
    {
        /* Viewとのバインド用プロパティ */
        // 名称
        public ReactiveProperty<string> Text { get; private set; }
        // 選択フラグ
        public ReactiveProperty<bool> IsSelected { get; private set; }
        // 展開フラグ
        public ReactiveProperty<bool> IsExpanded { get; private set; }
        // 子要素
        public ReadOnlyReactiveCollection<DirectoryTreeViewModel> Children { get; private set; }

        /* 内部処理用のプロパティ */
        // ディレクトリ
        public DirectoryInfo Directory { get; private set; }
        // 親ノード
        public DirectoryTreeViewModel ParentNode { get; private set; }
        // Dispose用
        public CompositeDisposable Disposable { get; private set; }

        // 子孫要素を全て取得
        public IEnumerable<DirectoryTreeViewModel> GetAllChildrenEnumerable()
        {
            Queue<DirectoryTreeViewModel> queue = new Queue<DirectoryTreeViewModel>();
            foreach (var node in this.Children)
                queue.Enqueue(node);

            while(queue.Count>0)
            {
                var node = queue.Dequeue();
                yield return node;
                foreach (var child in node.Children)
                    queue.Enqueue(child);
            }
        }

        // ルート情報を作成
        public DirectoryTreeViewModel()
        {
            this.Text = new ReactiveProperty<string>("ルート");
            this.IsSelected = new ReactiveProperty<bool>(false);
            this.IsExpanded = new ReactiveProperty<bool>(false);
            this.Children = DirectoryExtensions.GetRootDirectories()
                .Select(x => new DirectoryTreeViewModel(x, this))
                .ToObservable()
                .ToReadOnlyReactiveCollection();
        }

        // ノード情報を作成
        public DirectoryTreeViewModel(DirectoryInfo directory, DirectoryTreeViewModel parent)
        {
            this.Disposable = new CompositeDisposable();
            this.Directory = directory;
            this.ParentNode = parent;

            this.Text = new ReactiveProperty<string>(this.Directory.Name);
            this.IsSelected = new ReactiveProperty<bool>(false);
            this.IsExpanded = new ReactiveProperty<bool>(false);

            // 子要素のディレクトリ情報取得用のObservable
            // アクセス不可の場合は空情報を作成する
            var childrenObservable = Observable.Defer(() => this.Directory.EnumerateDirectories().ToObservable())
                .Catch((UnauthorizedAccessException ex) => Observable.Empty<DirectoryInfo>())
                .Select((x, i) => CollectionChanged<DirectoryTreeViewModel>.Add(i, new DirectoryTreeViewModel(x, this)));

            // ディレクトリツリー変更通知を受け取る
            var watcherObservable = FileSystemWatcherObservable.GetObservable(this.Directory);
            // 現ノードの直近の子情報のイベントのみを処理する
            watcherObservable = watcherObservable.Where(x =>
            {
                string parentPath = new string(
                    x.Path.Reverse()
                        .SkipWhile(y => y != Path.DirectorySeparatorChar)
                        .Skip(1)
                        .Reverse()
                        .ToArray()
                    );
                return parentPath == this.Directory.FullName.Trim('\\');
            });

            // Createdイベントでノード追加のCollectionChangedを作成
            var createEvent = watcherObservable.Where(x => x.EventType == FileSystemWatcherObservable.ChangeEventType.Create)
                // 同一名称が存在しないか確認
                .Where(x =>
                {
                    return !this.Children.Any(y => y.Text.Value == Path.GetFileName(x.Path));
                })
                .Select(x =>
                {
                    var name = Path.GetFileName(x.Path);
                    // 文字列比較して挿入位置のノードを取得
                    var node = this.Children.FirstOrDefault(y => y.Text.Value.CompareTo(name) > 0);
                    // 追加するindex位置を取得
                    int index;
                    if (node == null)
                        index = this.Children.Count;
                    else
                        index = this.Children.IndexOf(node);
                    // 要素追加
                    return CollectionChanged<DirectoryTreeViewModel>.Add(index, new DirectoryTreeViewModel(new DirectoryInfo(x.Path), this));
                });

            // Deleteイベントでノード削除のCollectionChangedを作成
            var deleteEvent = watcherObservable.Where(x => x.EventType == FileSystemWatcherObservable.ChangeEventType.Delete)
                // 削除対象が存在するか確認
                .Where(x =>
                {
                    return this.Children.Any(y => y.Text.Value == Path.GetFileName(x.Path));
                })
                .Select(x =>
                {
                    var name = Path.GetFileName(x.Path);
                    // 文字列比較して削除位置のノードを取得
                    var node = this.Children.FirstOrDefault(y => y.Text.Value == name);
                    // 削除するindex位置を取得
                    int index = this.Children.IndexOf(node);
                    return CollectionChanged<DirectoryTreeViewModel>.Remove(index);
                });

            // 子要素のコレクション情報更新・削除用のObservableを作成
            var mergeObservable = Observable
                .Merge(
                    this.ParentNode.IsExpanded
                        .Where(x => x)
                        .Take(1)
                        .SelectMany(y => Observable.Concat(childrenObservable, createEvent.ObserveOnUIDispatcher())),
                    deleteEvent.ObserveOnUIDispatcher()
                )
                .Publish();
            this.Children = mergeObservable.ToReadOnlyReactiveCollection();
            mergeObservable.Connect()
                .AddTo(this.Disposable);

            // 要素削除された時の後処理
            this.Children.ObserveRemoveChangedItems()
                .Subscribe(x =>
                {
                    foreach(var deleteNode in x)
                    {
                        deleteNode.Disposable.Dispose();
                        foreach(var child in deleteNode.GetAllChildrenEnumerable().ToArray())
                        {
                            child.Disposable.Dispose();
                        }
                    }
                })
                .AddTo(this.Disposable);
        }
    }

    public class FileSystemWatcherObservable
    {
        public enum ChangeEventType
        {
            Create, Delete
        };

        public string Path { get; private set; }
        public ChangeEventType EventType { get; private set; }

        public FileSystemWatcherObservable(string path, ChangeEventType type)
        {
            this.Path = path;
            this.EventType = type;
        }

        public static IObservable<FileSystemWatcherObservable> GetObservable(DirectoryInfo directory)
        {
            return saveDict[directory.Root.FullName];
        }

        // ディレクトリツリー変更監視用のIObservable保存用ディクショナリ
        static Dictionary<string, IObservable<FileSystemWatcherObservable>> saveDict = new Dictionary<string, IObservable<FileSystemWatcherObservable>>();

        // ルートディレクトリ毎に変更監視用のIObservableを作成
        static FileSystemWatcherObservable()
        {
            foreach (var item in DirectoryExtensions.GetRootDirectories())
            {
                saveDict.Add(item.Root.FullName, CreateObservable(item));
            }
        }

        // ディレクトリツリー変更監視用のIObservable作成処理
        static IObservable<FileSystemWatcherObservable> CreateObservable(DirectoryInfo directory)
        {
            var subject = new Subject<FileSystemWatcherObservable>();

            // FileSystemWatcher作成
            var watcher = new FileSystemWatcher();
            // ディレクトリのルートをパスに設定
            watcher.Path = directory.Root.FullName;
            // ディレクトリの変更情報のみを通知
            watcher.NotifyFilter = NotifyFilters.DirectoryName;
            // サブディレクトリ以下も監視する
            watcher.IncludeSubdirectories = true;

            // Createdイベントを加工
            var createEvent = watcher.CreatedAsObservable()
                .Select(x => new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Create));
            // .Deletedイベントを加工
            var deleteEvent = watcher.DeletedAsObservable()
                .Select(x => new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Delete));
            // Renamedイベントを加工
            // RenamedイベントはDeleteとCreateの2つに分割する
            var renameEvent = watcher.RenamedAsObservable()
                .SelectMany(x =>
                {
                    return new[]
                    {
                        new FileSystemWatcherObservable(x.OldFullPath, ChangeEventType.Delete),
                        new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Create)
                    }
                    .ToObservable();
                })
                .Publish();

            // 加工したFileSystemWatcherをマージして値を発行する
            Observable
                .Merge(
                    ThreadPoolScheduler.Instance,
                    createEvent,
                    deleteEvent,
                    renameEvent
                )
                .Subscribe(
                    x => subject.OnNext(x),
                    ex => subject.OnError(ex),
                    () => subject.OnCompleted());

            renameEvent.Connect();

            watcher.EnableRaisingEvents = true;
            return subject.AsObservable();
        }
    }

    public static class FileSystemWatcherExtensions
    {
        /// <summary>
        /// ChangedイベントをObservable化
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public static IObservable<FileSystemEventArgs> ChangedAsObservable(this FileSystemWatcher source)
        {
            return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
                h => (sender, args) => h(args),
                h => source.Changed += h,
                h => source.Changed -= h);
        }

        /// <summary>
        /// CreatedイベントをObservable化
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public static IObservable<FileSystemEventArgs> CreatedAsObservable(this FileSystemWatcher source)
        {
            return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
                h => (sender, args) => h(args),
                h => source.Created += h,
                h => source.Created -= h);
        }

        /// <summary>
        /// DeletedイベントをObservable化
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public static IObservable<FileSystemEventArgs> DeletedAsObservable(this FileSystemWatcher source)
        {
            return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
                h => (sender, args) => h(args),
                h => source.Deleted += h,
                h => source.Deleted -= h);
        }

        /// <summary>
        /// RenamedイベントをObservable化
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public static IObservable<RenamedEventArgs> RenamedAsObservable(this FileSystemWatcher source)
        {
            return Observable.FromEvent<RenamedEventHandler, RenamedEventArgs>(
                h => (sender, args) => h(args),
                h => source.Renamed += h,
                h => source.Renamed -= h);
        }
    }

    public static class DirectoryExtensions
    {
        /// <summary>
        /// ルート要素のディレクトリ情報を取得
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<DirectoryInfo> GetRootDirectories()
        {
            // とりあえず固定ドライブのみを取得
            return Environment.GetLogicalDrives()
                .Where(x => new DriveInfo(x).DriveType == DriveType.Fixed)
                .Select(x => new DirectoryInfo(x));
        }
    }
}


* MainWindow.xaml

<Window x:Class="DirectoryTreeViewSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:DirectoryTreeViewSample"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <Grid>
        <TreeView Margin="0" Grid.Column="0" ItemsSource="{Binding DirectoryTree}" >
            <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="IsSelected" Value="{Binding Path=IsSelected.Value, Mode=TwoWay}"/>
                    <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded.Value, Mode=TwoWay}"/>
                </Style>
            </TreeView.ItemContainerStyle>
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <StackPanel Orientation="Horizontal" Height="20">
                        <TextBlock Text="{Binding Text.Value}"/>
                    </StackPanel>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>

Reactive Extensions 2.2 まとめ(1)

ReactiveExtensions 2.2.4を勉強した事のまとめ(随時更新中)

Taskとの相互変換

IObservableとTaskで相互に変換できる。
詳細な情報は@xin9leさんのブログ(Rxとの相互運用)が分かりやすいと思います。

■IObservableからTaskへの変換
  • ToTaskメソッドでTaskに変換。
  • OnComplete発行でTaskの処理完了する。
  • OnComplete直前のOnNextの値がTask.Resultになる。
var task = Observable.Range(1, 3).ToTask(); // IObservableをTaskに変換
Console.WriteLine(await task); // taskの結果を表示(3が表示される)
■TaskからIObservableへの変換
  • ToObservableメソッドでIObservableに変換。
  • IObservableの他の非同期系の物と動作が一緒で、処理中はHot、処理完了したらCold(Subscribeする度にキャッシュしているOnComplete直前のOnNextの値を発行)になる。
var task = Task.Run(() => "ToObservable"); // 文字列を返すTask生成
var observable = task.ToObservable(); // TaskをIObservableに変換
observable.Subscribe(x => Console.WriteLine(x));


await

IObservableもawaitする事が出来るようになった。
詳細な情報は下記のサイトが分かりやすいと思います。
@xin9leさんのブログ(Rxとの相互運用)
@neueccさんのブログ(Reactive Extensions v1.1.11011.11リリースに見る.NET 4.5からの非同期処理)

  • IObservableに対してawaitすることで非同期で値を取り出す事が出来る。
  • 取得出来る値はOnComplete直前のOnNextの値(他の非同期系のIObservableと同じような動き)。
var result = await Observable.Range(1, 3); // awaitで値取り出し
Console.WriteLine(result); // 結果表示(3)


Wait

Waitメソッドが追加された。詳細な情報は下記のサイトが分かりやすいと思います。
@neueccさんのブログ(Reactive Extensions v1.1.11011.11リリースに見る.NET 4.5からの非同期処理)

  • 同期的に最後の値を取得
  • Lastと同じ動作
  • 元々あった同期的に値を取り出すメソッド(First等)がObsoleteになり
    その代わりとして追加された?
var result = Observable.Range(1, 3).Wait(); // 同期的に最後の値を取得
Console.WriteLine(result); // 結果表示(3)


FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, SingleAsync, SingleOrDefaultAsync

同期的に値を取得するメソッドがObsoleteになり
その代わりに非同期で取得出来るメソッドが追加された?
詳細な情報は下記のサイトが分かりやすいと思います。
@neueccさんのブログ(Reactive Extensions v1.1.11011.11リリースに見る.NET 4.5からの非同期処理)

非推奨メソッド 追加メソッド
First FirstAsync
FirstOrDefault FirstOrDefaultAsync
Last LastAsync
LastOrDefault LastOrDefaultAsync
Single SingleAsync
SingleOrDefault SingleOrDefaultAsync
var ids = Observable.Range(1,3);
var result1 = await ids.FirstAsync(); // 非同期で取得(1の値を取得)
var result2 = ids.FirstAsync().Wait(); // 同期的に取得(1の値を取得)


FromAsyncPattern

FromAsyncPatternメソッドがObsoleteに変更された。
詳細な情報は下記のサイトが分かりやすいと思います。
@neueccさんのブログ(Reactive Extensions v1.1.11011.11リリースに見る.NET 4.5からの非同期処理)

  • .NET4.5から非同期系のメソッド(***Async)が追加されたのが理由?
  • TaskFactory.FromAsyncを使いTaskを取得しToObservableで
    IObservableに変換する事で非推奨のメソッドを使わずIObservable化は可能


ForEach、ForEachAsync

ForEachメソッドがObsoleteになり新たにForEachAsyncメソッドが追加された。

  • ForEachAsyncメソッドを使うことにより非同期にForEachの処理が出来る。
  • ForEachメソッドでは、ForEachを呼んだスレッドで同期的に実行されていたので処理が
    終了するまでスレッドがブロックされる。
  • ForEachAsyncでは引数で渡されたIObservableに対してSubscribeして
    OnNextで発行された値に対してactionを実行しているので
    IObservableがカレントスレッドで動作している場合はスレッドがブロックされるので
    非同期に動作させたい場合はスレッドプール上等で動かす必要がある。
// Rangeがカレントスレッドで動くのでawaitしているのに制御戻らず
// ForEachAsyncの処理が終わるまでスレッドブロックされる
await Observable.Range(1, 10000000)
    .ForEachAsync(x => Console.WriteLine(x));


// Rangeがスレッドプールで動くので非同期でForEachAsyncが処理される
await Observable.Range(1, 10000000, System.Reactive.Concurrency.ThreadPoolScheduler.Instance)
    .ForEachAsync(x => Console.WriteLine(x));


// ForEachの場合はRangeをスレッドプールで動作させても
// ForEachが同期的に実行されるので呼んだスレッドが処理終了されるまでブロックされる
Observable.Range(1, 10000000, System.Reactive.Concurrency.ThreadPoolScheduler.Instance)
    .ForEach(x => Console.WriteLine(x));
  • ForEachAsyncは非同期で処理できるが、引数に渡せるのは
    Action型なのでOnNext毎に実行される処理を非同期にしてしまうと正しく完了待機できない
    OnNext毎のactionの処理も非同期にしたい場合はSelectManyを使う
    (詳細は@neueccさんのブログ(非同期時代のLINQ)参照)。


DelaySubscription

ディレイ処理のメソッドとして、DelaySubscriptionメソッドが追加された。

■Delayメソッドの動作

Delayメソッドは発行された値を後続に流すのを遅延する。
Delay中に発行された値をキャッシュしておき、指定時間経過後に後続に流す。

■DelaySubscriptionメソッドの動作

DelaySubscriptionメソッドは処理の開始を遅延する。
Subscribeしてから処理が開始されるまでの時間を遅延する。
遅延中にObservableから値が発行されていても無視する。
開始時間を遅延させるのでメソッドチェーン内のどの位置においても結果は変わらない。
メソッドチェーン内で何度も呼んだ場合はトータルの時間分処理の開始を遅延する。

■動作確認用のObservable作成
// 1,2を発行するObservable(1秒間隔で発行)
var delayTestObservable = Observable.Create<int>(observer =>
    {
        Console.WriteLine("{0:HH:mm:ss}:OnNext(1)", DateTime.Now);
        observer.OnNext(1);

        // 1秒スリープ
        System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));

        Console.WriteLine("{0:HH:mm:ss}:OnNext(2)", DateTime.Now);
        observer.OnNext(2);
        observer.OnCompleted();
        return () => { };
    });
■Delayメソッドの動作確認

Delayメソッドより後の処理は、発行された値を渡すのが遅延されている。
Delayメソッドより前の処理は、遅延されず動き続ける。

Console.WriteLine("{0:HH:mm:ss}:StartMethod", DateTime.Now);
delayTestObservable
    // OnNextされたらすぐに処理される
    .Do(x => Console.WriteLine("{0:HH:mm:ss}:Do1({1})", DateTime.Now, x))
    // 5秒遅延させる
    .Delay(TimeSpan.FromSeconds(5))
    // 5秒遅延1回なので、OnNextから5秒後に処理される
    .Do(x => Console.WriteLine("{0:HH:mm:ss}:Do2({1})", DateTime.Now, x))
    // 5秒遅延させる
    .Delay(TimeSpan.FromSeconds(5))
    // 5秒遅延2回なので、OnNextから10秒後に処理される
    .Subscribe(x => Console.WriteLine("{0:HH:mm:ss}:Subscribe({1})", DateTime.Now, x));


/* 実行結果
10:36:54:StartMethod
10:36:54:OnNext(1)
10:36:54:Do1(1)
10:36:55:OnNext(2)
10:36:55:Do1(2)
10:36:59:Do2(1)
10:37:00:Do2(2)
10:37:04:Subscribe(1)
10:37:05:Subscribe(2)
*/
■DelaySubscriptionメソッドの動作確認

SubScribeされてから実際に処理が開始されるまでが遅延されている。

Console.WriteLine("{0:HH:mm:ss}:StartMethod", DateTime.Now);
delayTestObservable
    //OnNextされたらすぐに処理される
    .Do(x => Console.WriteLine("{0:HH:mm:ss}:Do1({1})", DateTime.Now, x))
    //処理の開始を5秒遅延させる
    .DelaySubscription(TimeSpan.FromSeconds(5))
    //OnNextされたらすぐに処理される
    .Do(x => Console.WriteLine("{0:HH:mm:ss}:Do2({1})", DateTime.Now, x))
    //処理の開始をを更に5秒遅延させる
    .DelaySubscription(TimeSpan.FromSeconds(5))
    //OnNextされたらすぐに処理される
    .Subscribe(x => Console.WriteLine("{0:HH:mm:ss}:Subscribe({1})", DateTime.Now, x));


/* 実行結果
10:46:43:StartMethod
10:46:53:OnNext(1)
10:46:53:Do1(1)
10:46:53:Do2(1)
10:46:53:Subscribe(1)
10:46:54:OnNext(2)
10:46:54:Do1(2)
10:46:54:Do2(2)
10:46:54:Subscribe(2)
*/
■DelayメソッドでDelaySubscriptionメソッドと同じ動きをさせるには?
// 開始処理を遅延させたいObservableを定義
var ids = Observable.Range(1, 3)
    .Do(x => Console.WriteLine("{0:HH:mm:ss}:Range OnNext({1})", DateTime.Now, x));


// Delayを単純に呼んだ場合
Console.WriteLine("{0:HH:mm:ss}:Start", DateTime.Now);
ids.Delay(TimeSpan.FromSeconds(5))
    .Subscribe(x => Console.WriteLine("{0:HH:mm:ss}:Subscribe OnNext({1})", DateTime.Now, x));

/* 処理結果:SubscribeのOnNext処理が遅延しているだけでidsの開始処理は遅延されていない
13:00:01:Start
13:00:01:Range OnNext(1)
13:00:01:Range OnNext(2)
13:00:01:Range OnNext(3)
13:00:06:Subscribe OnNext(1)
13:00:06:Subscribe OnNext(2)
13:00:06:Subscribe OnNext(3)
*/


// Emptyに対してDelayしConcatで開始処理を遅延したいObservableを渡す
Console.WriteLine("{0:HH:mm:ss}:Start", DateTime.Now);
Observable.Empty<int>()
    .Delay(TimeSpan.FromSeconds(5))
    .Concat(ids)
    .Subscribe(x => Console.WriteLine("{0:HH:mm:ss}:Subscribe OnNext({1})", DateTime.Now, x));

/* 処理結果:idsの開始処理も遅延されている
13:08:03:Start
13:08:08:Range OnNext(1)
13:08:08:Subscribe OnNext(1)
13:08:08:Range OnNext(2)
13:08:08:Subscribe OnNext(2)
13:08:08:Range OnNext(3)
13:08:08:Subscribe OnNext(3)
*/

ReactivePropertyのソース見れば
OnErrorRetryメソッドでDelayを使った開始処理の遅延がありますのでそっちを参考にしましょう。


Create

// 5秒毎に値を発行するIObservable
Observable.Create<int>(async observer =>
    {
        // 値発行
        observer.OnNext(1);
        // 5秒待機(※非同期なメソッド呼び出し Taskを返すデリゲートなのでawait利用できる)
        await Task.Delay(TimeSpan.FromSeconds(5));
        // 値発行
        observer.OnNext(2);
        // 5秒待機(※非同期なメソッド呼び出し Taskを返すデリゲートなのでawait利用できる)
        await Task.Delay(TimeSpan.FromSeconds(5));
        // 値発行
        observer.OnNext(3);
        // 完了
        observer.OnCompleted();
        // Disposeされた時に実行されるActionを返す
        return () => { };
    })
    .Subscribe(x => Console.WriteLine("{0:HH:mm:ss}:Subscribe OnNext({1})", DateTime.Now, x));

/* 処理結果:5秒毎に値が発行されている
13:53:05:Subscribe OnNext(1)
13:53:10:Subscribe OnNext(2)
13:53:15:Subscribe OnNext(3)
*/


Defer DeferAsync

■Deferメソッド
  • Taskを受け取るオーバーロードが増えた
    Func<Task<IObservable<TResult>>>を引数に渡す。
  • 非同期にIObservableを生成するメソッドをIObservable化する?
  • 内部の処理では、Func<Task<IObservable<TResult>>>をStartAsyncに渡して、IObservable<IObservable<TResult>>を取得し、それをMergeでばらしてIObservable<TResult>に変換している?
// 非同期でIObservable<int>を生成するメソッド
private async static Task<IObservable<int>> CreateObservableRangeAsync()
{
    // 5秒待機
    await Task.Delay(TimeSpan.FromSeconds(5));
    // IObservable<int>を生成
    return Observable.Range(1, 3);
}

// 非同期でIObservable<int>を生成するTaskをIObservable化
// Task<IObservable<int>> => IObservable<IObservable<int>> => IObservable<int> の順番で変換される
// StartAsync(ToObservable) => Merge の順番でメソッドが呼ばれている
IObservable<int> defer = Observable.Defer(CreateObservableRangeAsync);

// Subscribe
defer.Subscribe(Console.WriteLine);

/* 結果表示
1
2
3
*/
■DeferAsyncメソッド
  • Deferとの違いはCancellationTokenを引数に貰えるだけ?


StartAsync

  • Taskを返すメソッドをIObservable化出来る。
  • Func<Task>, Func<Task<TResult>>, Func<CancellationToken,Task>, Func<CancellationToken,Task<TResult>>を引数に渡せる。
  • 引数無しのFuncは、TaskをToObservableするのと同じ。
  • CancellationTokenを引数に受け取るFuncは、購読解除時(Dispose)に
    CancellationTokenSourceのCancelを呼んでくれる。
// Taskを返すメソッドをIObservable化
// TaskをToObservableするのと変わらない
Observable.StartAsync(() =>
    {
        return Task.Run(() => "StartAsync");
    })
    .Subscribe(Console.WriteLine);


/* 結果表示
StartAsync
*/


// CancellationTokenを引数に貰うオーバーロード
// 購読解除した時にCancellationTokenSource.Cancelを呼んでくれる
// TaskをToObservableで変換した場合は購読解除してもCancelは呼んでくれないのでTaskの処理は終了するまで動き続けている
var dispose = Observable.StartAsync(token =>
    {
        return Task.Run(async () =>
            {
                try
                {
                    // Cancelするまで無限ループ
                    while (true)
                    {
                        // Cancelされたら例外発行
                        token.ThrowIfCancellationRequested();
                        // 1秒待機
                        await Task.Delay(TimeSpan.FromSeconds(1));
                    }
                    return "StartAsync Cancel";
                }
                catch (OperationCanceledException ex)
                {
                    // cancelされた時にメッセージ表示
                    Console.WriteLine(ex.Message);
                    throw;
                }
            });
    })
    .Subscribe(Console.WriteLine);

// すぐに購読解除
dispose.Dispose();


/* 結果表示
操作はキャンセルされました。
*/


FromAsync

  • Taskを返すメソッドをIObservable化出来る。
  • StartAsyncをDeferで包んでいる。
  • Deferで包んでいるのでSubscribeするまでTaskの処理は開始されない。


Using

  • Task関係のオーバーロードが追加されている
  • Taskの処理を確実に終了してくれる?


Schedulerの指定

Scheduleの指定の仕方が変わった。
今までは、スレッドプール上で実行したい場合はScheduler.ThreadPool等、Schedulerクラスで定義されているIScheduleを渡していたがObsoleteに変更された。

■新しいSchedulerの指定に使う定義(一部)

  • System.Reactive.Concurrency.DefaultScheduler.Instance
  • System.Reactive.Concurrency.CurrentThreadScheduler.Instance
  • System.Reactive.Concurrency.ImmediateScheduler.Instances
  • System.Reactive.Concurrency.ThreadPoolScheduler.Instance
  • System.Reactive.Concurrency.NewThreadScheduler.Default
  • System.Reactive.Concurrency.TaskPoolScheduler.Default
// スレッドプール上で実行
// 非推奨となったスケジューラの指定方法
Observable.Range(1, 10000, System.Reactive.Concurrency.Scheduler.ThreadPool)
    .Subscribe(Console.WriteLine);

// スレッドプール上で実行
// 今後推奨されるスケジューラの指定方法
Observable.Range(1, 10000, System.Reactive.Concurrency.ThreadPoolScheduler.Instance)
    .Subscribe(Console.WriteLine);


合成処理

合成のメソッドでTaskを渡せるようになった。
Task<TSource>を受け取るメソッド(SelectMany)
IObservable<Task<TSource>>を受け取るメソッド(Merge等)

  • Task<TSource>を受け取るメソッドは内部でTask<TSource>をToObservableしている。
  • IObservable<Task<TSource>>を受け取るメソッドは内部でSelect呼んでToObservableしている。


Rx勉強するのに参考にしているサイト


個人的なメモ

  • Visual Studioデバッグ設定で「マイコードのみを有効にする」のチェックをはずしていた方がよい?
    これ有効にしてるとOnErrorの処理等記述していてもマイコードで例外をキャッチしていないので例外発生するたびにデバッガが捕捉するので面倒。

mono 3.4.0 をソースコードからインストール(debian7)

  • mono3.4.0をdebian7にインストール。
  • インストール先は/opt/mono/3.4.0/にインストールした。
  • rootユーザーで作業を行う。

必要なパッケージをインストール

■開発ツールをインストール

aptitude install make gcc g++ automake libtool

■libgdiplusのビルドに必要なパッケージをインストール

aptitude install pkg-config libglib2.0-dev libjpeg8-dev libpng12-dev libtiff4-dev libgif-dev libx11-dev libfreetype6-dev libfontconfig1-dev libxrender-dev libcairo2-dev libexif-dev

■ mono 3.4.0のビルドに必要なパッケージをインストール

aptitude install bison gettext

ソースコード保存用のフォルダを作成

mkdir -p /opt/src/mono/

libgdiplus をインストール

ソースコード保存用のフォルダに移動しlibgdiplusのソースコードを取得

cd /opt/src/mono/
wget http://download.mono-project.com/sources/libgdiplus/libgdiplus-2.10.9.tar.bz2

ソースコードを解凍して解凍ソースの中に移動

tar -jxvf libgdiplus-2.10.9.tar.bz2
cd libgdiplus-2.10.9/

■ インストール先を指定してコンパイル、インストール

./configure --prefix=/opt/mono/3.4.0/
make
make install

mono 3.4.0 をインストール

ソースコード保存用のフォルダに移動しmono3.4.0のソースコードを取得

cd /opt/src/mono/
wget http://download.mono-project.com/sources/mono/mono-3.4.0.tar.bz2

ソースコードを解凍して解凍ソースの中に移動

tar -jxvf mono-3.4.0.tar.bz2
cd mono-3.4.0/

■ インストール先を指定してコンパイル

./autogen.sh --prefix=/opt/mono/3.4.0 --with-libgdiplus=installed
make

■ インストール前にファイルを追加する(mono3.4.0ではmake installに必要なtargetファイルが1つ漏れているらしいので手動で追加する)

echo -e \
"<Project xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">" \
"\n\t<Import Project=\"..\Microsoft.Portable.Core.props\" />" \
"\n\t<Import Project=\"..\Microsoft.Portable.Core.targets\" />" \
"\n</Project>" > \
/opt/src/mono/mono-3.4.0/mcs/tools/xbuild/targets/Microsoft.Portable.Common.targets

■ mono3.4.0をインストール

make install

パスの設定

■ インストールしたmonoフォルダをパスに追加する、monoを使用するユーザーの.bashrcに追記

export PATH=/opt/mono/3.4.0/bin:$PATH

■ .bashrc反映

source ~/.bashrc

■ mono のバージョン確認で今回インストールしたバージョンが表示されるか確認

mono --version

Mono JIT compiler version 3.4.0 (tarball 2014415日 火曜日 17:55:32 JST)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com
        TLS:           __thread
        SIGSEGV:       altstack
        Notifications: epoll
        Architecture:  amd64
        Disabled:      none
        Misc:          softdebug
        LLVM:          supported, not enabled.
        GC:            sgen

EXEファイルの実行方法

■ VisualStudioで作ったexeファイルを持ってきてmonoコマンドでexeファイルを指定する

mono ConsoleApplication1.exe

C# LINQ to Objects (1)

LINQ to Objectsに関して勉強した部分のメモ1回目
基本的な部分のまとめ

  • Linq to Objectsとは
  • 遅延実行
  • 実体化

Linq to Objectsとは

IEnumeable<T>インターフェースを実装しているクラスに対して統一された操作(拡張メソッド)を提供するもの?

IEnumeable<T>インターフェースが実装されていれば、実体をあまり気にすることなくLinq to Objectsが提供する拡張メソッドを利用しデータに対しての処理を行う事が出来る。

Linq to Objectsが提供する拡張メソッド(一部のみ)

■ シーケンスの射影、要素の型と値を加工(遅延実行:実体化されない)
メソッド名 処理内容
Select 要素の型と値を加工する
SelectMany 2つのシーケンスの全ての組み合わせを行い、要素の型と値を加工する。
■ シーケンスのフィルタリング、要素絞込み(遅延実行:実体化されない)
メソッド名 処理内容
Where 条件を満たす要素を全て取得(フィルター処理)
Take 先頭から指定個数の要素を取得
TakeWhile 先頭から指定条件を満たす限り取得(途中で条件を満たさない要素があった場合はその時点で打ち切り)
Skip 先頭から指定個数の要素をスキップ
SkipWhile 先頭から指定条件を満たす限りスキップ(途中で条件を満たさない要素があった場合はその時点でスキップ終了)
■ シーケンスの結合(遅延実行:実体化されない)
メソッド名 処理内容
Join 一致するキーに基づいて2つのシーケンスを結合する
Zip 2つのシーケンスのマージを行う
■ シーケンスのグループ化(遅延実行:実体化されない)
メソッド名 処理内容
GroupBy 指定したキーでシーケンスのグループ化
GroupJoin 一致するキーに基づいて2つのシーケンスを結合した結果を指定したキーでグループ化
■ シーケンスから重複要素の除去(遅延実行:実体化されない)
メソッド名 処理内容
Distinct 1つのシーケンスから重複要素を除去し1つだけにする
■ シーケンスの結合(遅延実行:実体化されない)
メソッド名 処理内容
Concat 2つのシーケンスを結合する
■ シーケンスの集合演算(遅延実行:実体化されない)
メソッド名 処理内容
Union 2つのシーケンスの和集合を行う(重複する要素は除く)
Except 2つのシーケンスの差集合を行う
Intersect 2つのシーケンスの積集合を行う
■ シーケンスの並び替え(遅延実行:実体化されない)
メソッド名 処理内容
Reverse シーケンスの要素の順番を逆転させる
OrderBy シーケンスの要素を昇順で並び替える
ThenBy OrderByの結果を維持したままシーケンスの要素を昇順で並び替える
OrderByDescending シーケンスの要素を降順で並び替える
ThenByDescending OrderByDescendingの結果を維持したままシーケンスの要素を降順で並び替える
■ キャスト(遅延実行:実体化されない)
メソッド名 処理内容
Cast シーケンスの要素を指定した型にキャストする(キャストできない場合は例外発生)
OfType シーケンスの要素を指定した型にキャストする(キャストできない要素はシーケンスから除去する)
■ シーケンス作成(遅延実行:実体化されない)
メソッド名 処理内容
Range 指定値から1ずつ昇順で指定回数繰り返すシーケンスを作成する
Repeat 指定値を指定回数繰り返すシーケンスを作成する
■ 要素取得(即時実行:実体化される)
メソッド名 処理内容
First 条件を満たす先頭要素取得(取得できない場合は例外発生)
FirstOrDefault 条件を満たす先頭要素取得(取得できない場合は例外発生しない)
Last 条件を満たす最終要素取得(取得できない場合は例外発生)
LastOrDefault 条件を満たす最終要素取得(取得できない場合は例外発生しない)
ElementAt 指定Index位置の要素取得(取得できない場合は例外発生)
ElementAtOrDefault 指定Index位置の要素取得(取得できない場合は例外発生しない)
■ シーケンスの集計(即時実行:実体化される)
メソッド名 処理内容
Count シーケンスの要素数を取得
Max シーケンスの最大値を取得
Min シーケンスの最小値を取得
Sum シーケンスの合計値を取得
■ シーケンスが条件を満たすか判定(即時実行:実体化される)
メソッド名 処理内容
Any シーケンスの要素が一つでも条件を満たすか判定する(1つでも条件を満たせば処理を即終了、trueを返す)
All シーケンスの要素全てが条件を満たすか判定する
■ シーケンスからコレクションクラスへの変換(即時実行:実体化される)
メソッド名 処理内容
ToArray シーケンスから配列を作成する
ToList シーケンスからListを作成する
ToDictionary シーケンスからDictionaryを作成する
ToLookup シーケンスからILookupを作成する(1つのキーに対して複数の値を持てる読み取り専用のDictionaryのような物?)

遅延実行

  • Linq to Objectsの提供する拡張メソッドは遅延実行される物と即時実行される物がある。
  • 遅延実行される物は実際の処理は実体化されるまで行われない。

実体化

■ 実体化されるタイミング

  • 即時実行されるメソッドを呼ぶ(メソッド呼ばれたタイミングで要素毎にクエリが実行される)
  • foreachが実行される(ループ回る度に要素毎に定義したクエリが実行される)
// 1~10万までの値を表すシーケンス  
var datas = Enumerable.Range(1, 100000);

// 先頭100要素だけ欲しいのでTakeメソッドでフィルタリングする  
var takeDatas = datas.Take(100); 
// この時点ではまだ処理実行されておらず、クエリを定義しただけの状態、
// 実体化された際にシーケンスの要素毎にクエリが実行される、今回はTakeなので100要素取り出した時点で処理が終了するが  
// Whereだと全要素チェックするので全要素分定義したクエリが実行される  

// 実体化1 即時実行されるメソッドを呼ぶ  
var ArrayDatas = takeDatas.ToArray(); // ここで実際に定義したクエリが実行され、シーケンスから先頭100要素を取得する処理が行われ、配列化される  

// 実体化2 foreachを呼ぶ  
foreach(var item in takeDatas)  
{  
    ・・・   // ループが回るたびに、要素毎に定義したクエリが実行される。  
}  

■ 初回の実体化は重い

  • 実体化されていないシーケンスの場合、初回の実体化処理は結構重い
  • 実体化されていないシーケンスに対してElementAtを呼んでIndex毎にアクセス等すると毎回実体化の処理が走ってすごく重いので、そういう場合は一度実体化を行い、実体化したシーケンスに対して処理を行う