2017年11月26日日曜日

ListView Extensions ver.1.0.0リリース

さて、ついに正式リリースしましたListView Extensions ver.1.0.0。

ListView Extensions - Nuget

beta4からはほとんど変わっていないはずです。そのままだと使いにくいWPFのListViewを使いやすくすることを目的に、View、ViewModel、Modelの全領域で便利な機能を提供するライブラリです。

ListView Extensionsの使用例はこんな感じです。


見た目はただのWPFのListViewに見えますが、よくよく見ると「Name」ヘッダーの上部にソートしていることを表す「▲」が表示されています。ListViewと言えばこのように名前、読み、年齢など様々なプロパティを持つデータをソートするときに使い、さらに、ユーザーが好きなプロパティでソートする機能を提供するようなものをイメージするはずです。Windowsのエクスプローラーだってそうですよね。ファイル名でソートしたり、更新日時でソートしたり、ファイルの種類でソートしたりすることがあるはずです。
ただ、WPFは標準でそのような機能をサポートしていません。自分でそれを実装しようとするととてつもなく手間がかかりますので、それをやってくれるライブラリが欲しくなります。そこで生まれたのがListView Extensionsというわけです。

せっかく正式リリースにしたので使い方をなぞってみます。

1. 使い方

実はbeta-1をリリースしたときにもある程度書いてあります。


ViewにはWPF標準のListView、ViewModelにはListViewViewModel、ModelにはSortableObservableCollectionを使うのが基本になります。それ以外の使い方はこのライブラリは想定していません。
SortableObservableCollectionは名前の通りソート可能コレクションです。必ずしもソートされているとは限らず、Sortメソッドを呼び出した時点でその時に指定したプロパティ名でソートします。いかにもListViewのソートに沿った機能ですね。

さて、Viewにはあらゆる機能が凝縮されていますので、これを見るのが手っ取り早いです。
まずはXAMLにListViewViewModelの名前空間を追加しておきましょう。
xmlns:lv="http://schemas.eh500-kintarou.com/ListViewExtensions"
Viewに対応するXAMLはこんな感じです。
<ListView ItemsSource="{Binding People}" >
    <ListView.Resources>
        <lv:SortingConditionConverter x:Key="ConditionToDirectionConverter" />
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Name" >
                    <lv:SortedHeader Content="Name" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Name'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="150" DisplayMemberBinding="{Binding Pronunciation}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Pronunciation" >
                    <lv:SortedHeader Content="Pronunciation" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Pronunciation'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="70" DisplayMemberBinding="{Binding Age}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Age" >
                    <lv:SortedHeader Content="Age" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Age'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Birthday}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Birthday" >
                    <lv:SortedHeader Content="Birthday" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Birthday'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="100" DisplayMemberBinding="{Binding Height}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Height_cm" >
                    <lv:SortedHeader Content="Height" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Height_cm'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
        </GridView>
    </ListView.View>
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Header="Increment the age" Command="{Binding IncrementAgeCommand}" />
                        <MenuItem Header="Decrement the age" Command="{Binding DecrementAgeCommand}" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
            <!--<Setter Property="lv:DoubleClickBehavior.Command" Value="{Binding DoubleClickCommand}" />-->
            <Setter Property="lv:DoubleClickBehavior.MethodTarget" Value="{Binding}" />
            <Setter Property="lv:DoubleClickBehavior.MethodName" Value="DoubleClicked" />
        </Style>
    </ListView.ItemContainerStyle>
    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="SelectedItemsMirroring" >
            <lv:ListViewSelectedItemsAction Source="{Binding People.SelectedItemsSetter}" />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
</ListView>

ちなみに、「People」がViewModelでのListViewViewModelのインスタンスになります。

順に機能を見ていきます。

a. ソート

ListViewの機能のよく使う機能としてリストのソートがありますが、WPFでは標準でサポートされていません。それを行います。

ヘッダーをクリックして並び替える

ListViewのヘッダーをクリックするとCommandが発生します。ですので、そこでソートをする機能を呼び出します。
/// <summary>
/// プロパティ名をパラメーターに与えてソートするコマンド
/// </summary>
public ICommand SortByPropertyCommand { get; }
ListViewViewModelにはプロパティ名でソートするコマンドが備わっています。ですので、ヘッダーのクリックに合わせてこのコマンドを呼ぶようにしましょう。プロパティ名はパラメーターで渡してあげる必要があります。

ヘッダーにソートを示す▲▼を表示する

これはWPFの標準の機能がないので、ListView Extensionsが提供するSortedHeaderコントロールをGridViewColumnHeaderContentとして使います。GridViewColumnHeaderはContentControlを継承しているので、任意のコントロールをContent表示することができるのです。

SortedHeaderコントロールもContentControlクラスを継承しており、1個だけプロパティが追加されています。
/// <summary>
/// ソートの方向
/// </summary>
public SortingDirection SortingDirection
{
    get { return (SortingDirection)GetValue(SortingDirectionProperty); }
    set { SetValue(SortingDirectionProperty, value); }
}
SortingDirectionもListView Extensionsが提供するEnumで、Ascending、Descending、Noneの3つの状態があります。これに合わせて▲▼を表示してくれるようになります。

しかし、ListViewViewModelはソート状態をソート方向、プロパティ名の2つの値で保持しています。それらをひっくるめて、SortingConditionというプロパティがあります。
/// <summary>
/// 現在のソート条件
/// </summary>
public SortingCondition SortingCondition
{
    get { ... }
    private set
    {
        ...
    }
}
ですので、これから各ヘッダーに対応したSortingDirectionに変換するValueConverterが必要になります。

ご安心ください。そのConverterもListView Extensionsには装備されています。
SortingConditionConverterがそれで、パラメーターにプロパティ名を与えてやればそのヘッダーのSortingDirectionを返してくれます。

というわけでこのようなXAMLが出来上がります。
<GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
    <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Name" >
        <lv:SortedHeader Content="Name" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Name'}" />
    </GridViewColumnHeader>
</GridViewColumn>
DisplayMemberBindingでプロパティ名、ヘッダーをクリックされたときのCommandParameterでプロパティ名、SortingConditionをSortingDirectionに変換するValueConverterのパラメーターでプロパティ名が必要で、計3回書く必要があります。SortedHeaderのContentもプロパティ名になる場合は4回出てきます。コピペミス等に気を付けてください。

b. アイテムのダブルクリック

これがなかなかやろうとすると難しいところです。
左クリックは通常選択程度の意味を成しませんからそんなに自分でコントロールしようと思うことはないですし、右クリックはたいていコンテキストメニューですからContextMenuプロパティにメニューを設定してやればいいです。しかし、ダブルクリックはアプリケーションを実行したり、編集画面を出したりと何かしらの処理を行う系になります。すなわち、ViewModelのメソッドやコマンドを呼び出したい何かになるわけです。

そこで、DoubleClickBehaviorです。この添付ビヘイビアはメソッド直接バインディングとコマンドのバインディングの両方を用意しています。Viewを見ればわかるかと思います。メソッドやコマンドは当然ですがアイテムのViewModelに書きます。

c. 選択

これもなかなか厄介です。ListViewのSelectedItemSelectedIndexは単一選択しか想定されていないうえに、SelectedItemsはgetのみでバインディングしようとするとうまくいきません。
ListViewItemはIsSelectedプロパティを持っているからこれを使えばいいのではと思うとそれも違います。ListViewItemは表示していない部分のインスタンスは消えてしまいますので、選択されているアイテムが表示範囲外に行くとIsSelectedは期待通りに動作してくれません。

実はListViewでの選択はこのSelectedItemsプロパティがすべて握っていて、このリストに対してAddやRemoveなどの編集操作をすると選択アイテムがそれに応じて増減します。さらに、このリストはICollectionChangedも実装していて、これで選択の変化を捉えることも可能です。
これはどう見てもコードビハインドで使うためのメソッドです。ですが、何とかそれをViewModelで使えるようにしました。手順としては次の通りになります。

SelectedItemsへの参照をViewModelに渡す

まずはこれを行う必要があります。これはListView Extensionsが提供しているListViewSelectedItemsActionを使います。ListViewViewModelはSelectedItemsSetterプロパティを持っており、ListViewSelectedItemsActionはこれにSelectedItemsの参照を渡してくれます。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="SelectedItemsMirroring" >
        <lv:ListViewSelectedItemsAction Source="{Binding People.SelectedItemsSetter}" />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
今回はLivetを使っているので、LivetのInteractionMessageTriggerを使っています。別のMVVMフレームワークでも、何かしらViewModelからViewのアクションを動かす機構を持っているはずなのでそれを使ってください。
タイミングは、例えばContentRenderedなどのListViewインスタンス生成直後に呼ばれるイベントのタイミングでコピーをすべきでしょう。そのタイミングでViewModelからViewにListViewSelectedItemsActionを動かすように呼び出します。
Messenger.Raise(new InteractionMessage("SelectedItemsMirroring"));

選択の読み書き

SelectedItemsは非ジェネリックのコレクションで非常に使いにくいです。ですので、ListViewViewModelのSelectedItemsSetterはSet-Onlyプロパティにしており、読み取りはできないようにしています。

選択の状態を読み取るには、SelectedItemsSetterをミラーリングしているSelectedItemsプロパティを使ってください。
/// <summary>
/// 選択中の項目をIListじゃ使いにくいから使いやすくミラーリングしたクラス
/// </summary>
public ReadOnlyObservableCollection<TViewModel> SelectedItems
{
    get
    {
        ...
    }
}
逆に、選択をするにはViewModelに用意されたメソッドを使います。
/// <summary>
/// 指定したアイテムを選択します
/// </summary>
/// <param name="item">選択するアイテム</param>
public void SelectItem(TViewModel item);

/// <summary>
/// 指定したインデックスの要素を選択します。
/// </summary>
/// <param name="index">選択するインデックス</param>
public void SelectAt(int index);

/// <summary>
/// 指定した要素の選択を解除します
/// </summary>
/// <param name="item">選択を解除する要素</param>
public void UnselectItem(TViewModel item);

/// <summary>
/// 指定したインデックスの要素の選択を解除します
/// </summary>
/// <param name="index">インデックス</param>
public void UnselectAt(int index);

/// <summary>
/// 選択されているアイテムかを調べます
/// </summary>
/// <param name="item">選択されているかどうか調べたい要素</param>
/// <returns>選択されていればtrue</returns>
public bool IsSelectedItem(TViewModel item);

/// <summary>
/// 選択されているアイテムかを調べます
/// </summary>
/// <param name="item">選択されているかどうか調べたい要素</param>
/// <returns>選択されていればtrue</returns>
public bool IsSelectedAt(int index);

/// <summary>
/// 指定したアイテムの選択を反転します
/// </summary>
/// <param name="item">選択を反転するアイテム</param>
public void ToggleItemSelection(TViewModel item);

/// <summary>
/// 指定したインデックスの要素の選択を反転します。
/// </summary>
/// <param name="index">インデックス</param>
public void ToggleItemSelectionAt(int index);
まあ、メソッドの使い方は見ればわかるでしょう。

選択に対する操作

さらにもう一歩先の機能として、選択に対する操作も用意しています。
リスト系のソフトだと、選択しているアイテムを削除したり、選択しているアイテムを上や下へ動かすという用途もたくさん出てくるでしょう。ListViewViewModelにはそのような操作をするコマンドも用意されています。
/// <summary>
/// 選択中の項目を削除するコマンド
/// </summary>
public ICommand RemoveSelectedItemCommand { get; }

/// <summary>
/// 選択中の項目を上へ移動するコマンド
/// </summary>
public ICommand MoveUpSelectedItemCommand { get; }

/// <summary>
/// 選択中の項目を上へ移動するコマンド
/// </summary>
public ICommand MoveDownSelectedItemCommand { get; }

/// <summary>
/// 選択を反転するコマンド
/// </summary>
public ICommand ToggleSelectionCommand { get; }
適当なボタンなどを作って、必要に応じてこのコマンドを呼び出してあげれば良いでしょう。

選択に関する制約

ListView.SelectedItemsプロパティを見てもらっても分かりますが、ListViewは「選択されているアイテム」で選択を管理しており、例えばインデックスなどでは管理されていません。これは何を意味しているかというと、ListViewの要素として同じインスタンスを複数登録すると選択が正常に動作しないということです。

ですが、通常はListViewの要素はアイテムのViewModelになります。ViewModelのインスタンスをあらゆるアイテムに対して生成しておけば同じインスタンスが複数登録されることはないでしょう。

これは、ListViewViewModelのコンストラクタで制御します。
public ListViewViewModel(ISortableObservableCollection<TModel> source, Func<TModel, TViewModel> converter, Dispatcher dispatcher) { ... }
第2引数のconverterは、Modelでのコレクションの要素をViewModelに変換するためのデリゲートです。ここでは、必ず
person => new PersonViewModel(person)

のように新しいインスタンスを作るように実装しておけばいいだけです。

d. スレッド安全性

ListView Extensionsはスレッドセーフに設計されています。
複数スレッドからの同時アクセスなどがあってもデータの整合性は維持できます。ただ、固有のデッドロックの問題等もありますので、十分注意して使ってください。
そのあたりは、beta4のときの記事に詳しく書いてあります。

2. サンプルプログラム

さて、機能の概略は説明しましたが、実際のプログラムは見てみないとわかりにくいところもあるかと思います。ですので、細かい部分はサンプルプログラムを参考にして使ってください。

ListViewExtensionsSample ver.1.0.0

 実はbeta1の時に公開したサンプルプログラムとほとんど変わっていません。ただ、Model側にてスレッド安全性を確かめるための激しいプログラムにしています。
void AddLoop()
{
    Task.Run(async () => {
        while(true) {
            await Task.Delay(TimeSpan.FromSeconds(1));
            
            Parallel.ForEach(Enumerable.Range(0, 200), p => {
                if(p % 2 == 0 && People.Count > 0) {
                    lock(People.SyncRoot)
                        People.RemoveAt(random.Next(People.Count));
                } else
                    Add();
            });                
        }
    });
}

1秒おきに複数スレッドから100回のデータの追加と100回のデータ削除を行っています。もしもスレッド安全性に問題があれば、このコードをしばらく実行していれば不具合が起きるでしょう。

3. 最後に

(少なくとも私としては)結構使いやすいライブラリになったのではないかなと自負しています。WPFのListViewで不便さを感じている皆さん、ぜひご活用ください!

定義に従って行列式を求める

ここでいう行列式の定義とはこれです。 \[{\rm det}A=\sum_{\sigma \in S_n} {\rm sgn}(\sigma)\prod_{i=1}^n a_{i \sigma(i)}\] ここで言う$\sigma$は置換で、すべての置換の総和で行列式を定義したものですね。ちなみに${\rm sgn}$は偶置換、奇置換それぞれで1,-1を取る関数です。詳しくは線形代数の教科書でも見てください。

この定義、何が恐ろしいって計算量が${\rm O}(n \times n!)$なんですよね。すべての置換パターンで積を計算しなければならないので、n個の数の順列分の計算量が生まれるというわけです。行列の次数が上がるにつれて指数オーダーよりも激しく計算量が増えていきます。
ただ、それぞれの置換パターンは独立していますので並列処理も可能です。ですので、ベンチマークに使いやすいわけです。

主たる課題は下記のとおりです。
  • n次の行列のときnつの数の積を取るためオーバーフローしやすい(=多倍長整数型などを作る必要がある)
  • すべての置換パターンを求める必要がある
  • 並列処理の効率化(クリティカルセクション等々)
大学1年生のときC言語で計算するプログラムを書いて遊んでいましたが、なかなか大変でした。でも、C#は様々なライブラリがあり、並列処理も簡単に書けます。というわけで、C#で書いてみました。
コンピューターでの小数には「精度」が伴ってしまいますので、行列の要素は整数に限定してプログラムを作っていきます。

多倍長整数

C#にはintやlongなどの型があります。それぞれ32bit、64bitとビット数は決まっており、それより巨大な計算結果になるような演算をすると正確な値が得られなくなってしまいます。

そこで、必要に応じてビット数を拡張し、特にビット数の縛りなくメモリが許す限りどんな大きな整数でも扱えるような機構が欲しくなります。C#のような言語ならばそういったクラスや構造体を作れば良でしょう。
というのは誰もが思うことで、すでにそういうのが用意されています。

BigInteger 構造体

使うときに参照を追加してやる必要がありますが、それだけで簡単に多倍長整数で計算してくれます。演算子もオーバーロードされており、特に違和感なく計算ができるところがとても良いです。

ですが、さすがにLINQのメソッドまでは用意されておりませんので、今回のプログラムで使う分を実装しておきましょう。

public static class BigIntegerEnumerable
{
    public static IEnumerable<BigInteger> Range(BigInteger start, BigInteger count)
    {
        for(BigInteger i = 0; i < count; i++)
            yield return start + i;
    }
}

public static class BigIntegerUtil
{
    public static BigInteger Factorial(int start) => Enumerable.Range(1, start).Aggregate(new BigInteger(1), (total, now) => total * now);

    public static BigInteger Sum(this IEnumerable<BigInteger> source) => source.Aggregate(BigInteger.Zero, (now, next) => now + next);

    public static BigInteger Product(this IEnumerable<BigInteger> source) => source.Aggregate(BigInteger.One, (now, next) => now * next);
}

置換は(1, 2, ..., n)と並んでいる数列を入れ替えていくという話なので、Enumerable.Rangeみたいなメソッドがあるととても便利です。
また、多倍長整数を使うモチベーションとして「階乗」というのは非常に大きいはずですが、なぜかBigInteger構造体には階乗を求めるメソッドが無いので実装しておきます。あとは総和と総乗を取るのでSumとProductという拡張メソッドを作っておきます。Aggregate大活躍です。

順列と置換

例えば(1,2,3)の3つの数の順列は、(1,2,3),(1,3,2),(2,1,3),(2,3,1),(3,1,2),(3,2,1)の6パターンあります。そして、順列を小さい順?に並べるとこの順序になるはずです。
じゃあ任意の数列に対してi番目の並び替えは何か、というのを求めるメソッドを用意します。

static int[] Permutation(int[] source, BigInteger index)
{
    if(source.Length <= 1)
        return source.ToArray();

    var list = source.ToList();
    var div = (int)BigInteger.DivRem(index, BigIntegerUtil.Factorial(source.Length - 1), out BigInteger remainder);

    var first = list[div];
    list.RemoveAt(div);

    return new int[] { first }.Concat(Permutation(list.ToArray(), remainder)).ToArray();
}

今の配列の一番前に来るべき数を決めて、残りの項目を1つの数列として再帰的にこの関数を呼び出すことでindex番目の並び順を求めています。

ただ、このindexと置換の回数は直接的な相関はありません(たぶん)。ですので、置換の回数を求めるメソッドも用意しました。

static int ReplaceCount(int[] array1, int[] array2)
{
    if(array1.Length != array2.Length)
        throw new ArgumentException("Invalid array lengthes");

    int count = 0;            

    for(int i = 0; i < array1.Length; i++) {
        if(array1[i] != array2[i]) {
            var nextIndex = array2.ToList().IndexOf(array1[i], i + 1);
            if(nextIndex < 0)
                throw new InvalidOperationException("Cannot find same value.");

            var swap = array2[i];
            array2[i] = array2[nextIndex];
            array2[nextIndex] = swap;
            count++;
        }
    }

    return count;
}

やってることは選択ソートみたいなもんです。選択ソートについて置換の回数を見ているので、別のアルゴリズムを使えば回数は異なる可能性があります。ただ、偶奇性は一意に定まります。

並列実装

ここまで来れば、定義に従って行列式を求めるだけです。

Random rnd = new Random();
int[,] matrix = new int[order, order];

for(int i = 0; i < order; i++) {
    for(int j = 0; j < order; j++)
        matrix[i, j] = rnd.Next(10);
}

for(int j = 0; j < order; j++) {
    for(int i = 0; i < order; i++)
        Console.Write($"{matrix[i, j]} ");
    Console.WriteLine();
}

Dictionary<int, BigInteger> threaddet = new Dictionary<int, BigInteger>();
object threadlock = new object();

Stopwatch sw = new Stopwatch();
sw.Start();

var source = Enumerable.Range(0, order).ToArray();
Parallel.ForEach(BigIntegerEnumerable.Range(0, BigIntegerUtil.Factorial(order)), patternIndex => {
    var pattern = Permutation(source, patternIndex);
    var res = source.Zip(pattern, (r, c) => (BigInteger)matrix[r, c]).Product() * (ReplaceCount(source, pattern) % 2 == 0 ? 1 : -1);

    var id = System.Threading.Thread.CurrentThread.ManagedThreadId;
    if(!threaddet.ContainsKey(id)) {
        lock(threadlock)
            threaddet.Add(id, 0);
    }
    threaddet[id] += res;
});

var det = threaddet.Values.Sum();

sw.Stop();

Console.WriteLine($"det = {det}");
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds}s");

まずはorder次の正方行列をランダムに生成します。わかりやすく各要素は0~9の整数にしています。
元の行列が決まったらorderの階乗だけ置換パターンがあるのでそれをParallel.ForEachで並列処理をしています。それぞれのパターンについて要素を取ってきて積を取ってから符号をつけます。
積が求まったら総和を取らなければなりませんが、和を取るためにはスレッド間での同期をとらなければなりません。ですので、それは一番最後に回すことにしています。具体的にはそれぞれのスレッドで計算した分の総和のみを求めており、すべてのスレッドの処理が終わったら各スレッドの総和を足し合わせる、という処理になっています。

スレッドの総和を求めるためには、スレッドIDをキーとしたDictionaryを用意して求めます。キーの追加時はロックをしないと、おそらく内部的なリストの「最後尾」にキーを追加というタイミングで複数スレッド間での競合が起きバグります。

私のPCはi7-4790Kを搭載していますが、11次の行列で2分半ちょっと計算にかかりました。行列を計算し始めてからOctaveを起動して行列を打ち込んで行列式を求めてもまだ定義に従った計算では求まらない、といったくらいのペースになります。

昔、C言語で作った時はえらく苦労をした記憶がありますが、C#だとこんなにすんなりできてしまいました。
やはり、並列化が体系化されていたり、演算子オーバーロード等で非常に使いやすい多倍長整数型が用意されていたり、あとはLINQで配列を簡単に扱えたりするのはとても大きいですね。

もうちょっとアルゴリズムが改良できるぞ、とかあったら是非教えてください。
特に置換パターンを求めたり置換回数を求めるあたりはまだ何かやりようがある気がしています。あ、「掃き出し法を使うと速いぞ」とか言うのは無しで。あくまでも定義に従った計算方式ということで。

2017年11月18日土曜日

MVVMのサンプルプログラム - TwitterViewer

前回の記事でMVVMとは何ぞやという説明をしましたが、今回はMVVMで実装する例としてTwitterのサンプルプログラムでも作ってみようかと思います。MVVMライブラリはLivetを使っています。

具体的なイメージを最初に持ってもらうために、完成形を最初に示しておきます。


短縮URLやRTの展開などはしていませんが、なかなかそれっぽいクライアントに見えるかと思います。

1. メインウィンドウのView

メインウィンドウのXAMLはこんな感じです。

<Window x:Class="TwitterViewer.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:TwitterViewer.Views"
        xmlns:vm="clr-namespace:TwitterViewer.ViewModels"
        Title="MainWindow" Height="1000" Width="500">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
    
        <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>

        <!--WindowのCloseキャンセル処理に対応する場合は、WindowCloseCancelBehaviorの使用を検討してください-->

    </i:Interaction.Triggers>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <Button Grid.Row="0" Content="Load" >
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click" >
                    <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="LoadButtonClicked" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <ListBox Grid.Row="1" ItemsSource="{Binding Statuses}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.CanContentScroll="False" >
            <ListBox.ItemTemplate>
                <DataTemplate DataType="{x:Type vm:TwitterStatusViewModel}">
                    <DockPanel HorizontalAlignment="Stretch">
                        <DockPanel.Background>
                            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1" >
                                <GradientStop Color="White" Offset="0" />
                                <GradientStop Color="#FFF0F0F0" Offset="1" />
                            </LinearGradientBrush>
                        </DockPanel.Background>
                        <Image DockPanel.Dock="Left" Source="{Binding IconSource}" Width="50" VerticalAlignment="Top" />
                        <DockPanel DockPanel.Dock="Top">
                            <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
                                <TextBlock Text="@" Foreground="Blue" FontSize="12" />
                                <TextBlock Text="{Binding ScreenName}" Foreground="Blue" FontSize="12" />
                                <TextBlock Text="{Binding UserName}" Foreground="Blue" FontSize="12" FontWeight="Bold" Margin="5,0,0,0" />
                            </StackPanel>
                            <TextBlock DockPanel.Dock="Right" Text="{Binding CreatedAt}" Foreground="DarkGray" FontSize="12" HorizontalAlignment="Right"/>
                        </DockPanel>
                        <TextBlock DockPanel.Dock="Bottom">
                            via 
                            <Hyperlink ToolTip="{Binding ViaLink}" >
                                <TextBlock Text="{Binding ViaName}" />
                                <i:Interaction.Triggers>
                                    <i:EventTrigger EventName="Click">
                                        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="ViaLinkClicked" />
                                    </i:EventTrigger>
                                </i:Interaction.Triggers>
                            </Hyperlink>
                        </TextBlock>
                        <TextBlock Text="{Binding Text}" FontSize="14" TextWrapping="Wrap" Margin="0,5,0,5" />
                    </DockPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
    </Grid>
</Window>

このクライアントはツイートを画面に表示する以外に、読み込みボタンとviaクライアントのクリックという2つの操作がユーザーでできるようになっています。
これはLivetのメソッド直接バインディング機能を使って、クリック時にViewModelのメソッドを呼び出すようにしています。

表示はListBoxのカスタマイズで行っています。
個々の要素(ツイート)の表示部分をテンプレートとしてListBox.ItemTemplateに渡しています。
若干複雑に見えるかもしれませんが、上の画像を見ながら追っていけば特段難しくないかと思います。

2. メインウィンドウのViewModel

メインウィンドウのViewModelはこんな感じです。

public class MainWindowViewModel : ViewModel
{
    Model model;

    public MainWindowViewModel()
    {
        model = Model.GetInstance();

        Statuses = model.Statuses.ToSyncedSynchronizationContextCollection(p => new TwitterStatusViewModel(p), System.Threading.SynchronizationContext.Current);
    }

    public void Initialize()
    {
    }

    public async void LoadButtonClicked()
    {
        await model.LoadHomeTimeline();
    }

    #region Statuses変更通知プロパティ

    public SynchronizationContextCollection<TwitterStatusViewModel> Statuses
    {
        get { return _Statuses; }
        set
        {
            if(_Statuses == value)
                return;
            _Statuses = value;
            RaisePropertyChanged(nameof(Statuses));
        }
    }
    private SynchronizationContextCollection<TwitterStatusViewModel> _Statuses;

    #endregion
}

Loadボタンが押されたときにそれを受け取るメソッドと、ツイートの表示内容を示すコレクションを用意しています。

SynchronizationContextCollectionはStatefulModelというライブラリのものです。「特定のコンテキストでコレクションの変更通知を発行するスレッドセーフなコレクション」で、WPFの制約である「単一スレッドでしかUIを触ることができない」を吸収してくれる便利なコレクションです。

ちなみに、Statusesプロパティはmodel.Statuses.ToSyncedSynchronizationContextCollectionメソッドで生成しています。ここでmodel.Statusesという変更通知コレクション(詳しくはModelの項を見てください)のイベントを拾えるようリスナーを登録しています。ViewModel側のStausesではModel側のイベントを拾って自身もそれに同期し変更通知イベントを発生させますが、その際にUIスレッドに転送してイベントを発生させてくれます。まさにMVVMのためのコレクション機構ですね。

3. メインウィンドウのModel

Modelはこちらです。

public class Model : NotificationObject
{
    #region Singleton

    static Model instance;

    public static Model GetInstance()
    {
        if(instance == null)
            instance = new Model();
        return instance;
    }

    #endregion

    TwitterContext twitterContext;

    private Model()
    {
        Statuses = new SortedObservableCollection<TwitterStatus, DateTime>(s => s.CreatedAt, true);

        SingleUserAuthorizer auth = new SingleUserAuthorizer()
        {
            CredentialStore = new SingleUserInMemoryCredentialStore()
            {
                ConsumerKey = "****",
                ConsumerSecret = "****",
                AccessToken = "****",
                AccessTokenSecret = "****",
            }
        };
        twitterContext = new TwitterContext(auth);
    }

    public async Task LoadHomeTimeline()
    {
        await Task.Run(() => {
            foreach(var status in twitterContext.Status.Where(p => p.Type == StatusType.Home && p.Count == 100)) {
                if(!Statuses.Any(p => p.StatusID == status.StatusID))
                    Statuses.Add(new TwitterStatus(status));
            }
        });
    }
    
    #region Statuses変更通知プロパティ

    public SortedObservableCollection<TwitterStatus, DateTime> Statuses
    {
        get { return _Statuses; }
        set
        { 
            if(_Statuses == value)
                return;
            _Statuses = value;
            RaisePropertyChanged(nameof(Statuses));
        }
    }
    private SortedObservableCollection<TwitterStatus, DateTime> _Statuses;

    #endregion        
}

TwitterにアクセスするのにはLinq2Twitterを使っています。
SortedObservableCollectionはStatefulModelの提供するクラスの1つです。タイムラインは常に時間順でソートされるべきですので、そうなるようにSortedなコレクションを使っています。ViewModel側でこれをウォッチしてUIのスレッドで変更通知をViewに伝えるのです。

ちなみに、このModelはシングルトンパターンを取っています。
ここはちょっとしたこだわりですが、ViewModelがModelを作るわけではないので、その気持ちを込めてこういう構造にしています。


4. ツイートのViewModel

さて、ここまで来てメインウィンドウに対応するViewModel、Model以外にもTwitterStatusViewModelとTwitterStatusというクラスがあることに気づいたことでしょう。個々のツイートに対応するViewがありますから、それに対応するViewModelを作るのは自然かと思います。

public class TwitterStatusViewModel : ViewModel
{
    TwitterStatus Source;

    public TwitterStatusViewModel(TwitterStatus source)
    {
        Source = source;

        Text = Source.Text;
        IconSource = Source.IconSource;
        ScreenName = Source.ScreenName;
        UserName = Source.UserName;
        CreatedAt = Source.CreatedAt;
        ViaName= Source.ViaName;
        ViaLink = Source.ViaLink;
    }

    #region Text変更通知プロパティ

    public string Text
    {
        get { return _Text; }
        set
        {
            if(_Text == value)
                return;
            _Text = value;
            RaisePropertyChanged(nameof(Text));
        }
    }
    private string _Text;

    #endregion

    #region IconSource変更通知プロパティ
    private string _IconSource;

    public string IconSource
    {
        get
        { return _IconSource; }
        set
        {
            if(_IconSource == value)
                return;
            _IconSource = value;
            RaisePropertyChanged(nameof(IconSource));
        }
    }
    #endregion

    #region ScreenName変更通知プロパティ

    public string ScreenName
    {
        get { return _ScreenName; }
        set
        {
            if(_ScreenName == value)
                return;
            _ScreenName = value;
            RaisePropertyChanged(nameof(ScreenName));
        }
    }
    private string _ScreenName;

    #endregion

    #region UserName変更通知プロパティ

    public string UserName
    {
        get { return _UserName; }
        set
        {
            if(_UserName == value)
                return;
            _UserName = value;
            RaisePropertyChanged(nameof(UserName));
        }
    }
    private string _UserName;

    #endregion
    
    #region CreatedAt変更通知プロパティ

    public DateTime CreatedAt
    {
        get { return _CreatedAt; }
        set
        { 
            if(_CreatedAt == value)
                return;
            _CreatedAt = value;
            RaisePropertyChanged(nameof(CreatedAt));
        }
    }
    private DateTime _CreatedAt;

    #endregion
    
    #region ViaName変更通知プロパティ

    public string ViaName
    {
        get { return _ViaName; }
        set
        {
            if(_ViaName == value)
                return;
            _ViaName = value;
            RaisePropertyChanged(nameof(ViaName));
        }
    }
    private string _ViaName;

    #endregion

    #region ViaLink変更通知プロパティ

    public string ViaLink
    {
        get { return _ViaLink; }
        set
        {
            if(_ViaLink == value)
                return;
            _ViaLink = value;
            RaisePropertyChanged(nameof(ViaLink));
        }
    }
    private string _ViaLink;

    #endregion

    public void ViaLinkClicked()
    {
        Source.OpenViaLink();
    }
}


やたら長いですが、大部分が変更通知プロパティです。
反射的に変更通知ができるように作ってしまいましたが、ツイートは一度つぶやくと後から変更できないので、Immutableに作っても良かってもよかったかもしれません。

今回は面倒なので実装しませんでしたが、例えば、ツイート時刻の相対表記(何秒前のツイートかを表示するもの)だったら定期的に表示内容を変えなければならないので、そういうものはModelの変更通知を監視し、変更されたら自身を更新するように作り替えねばなりません。

ちなみに、一番最後にはviaのリンクをクリックされたときに呼ばれるメソッドがありますが、これはそのままModelに横流ししています。

5. ツイートのModel

最後にツイートのModelです。

public class TwitterStatus : NotificationObject
{
    static Regex regViaUrl = new Regex("href=\"([^\"]+)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    static Regex regViaName = new Regex(">([^<]+)<", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public TwitterStatus(Status Source)
    {
        Text = Source.Text;
        StatusID = Source.StatusID;
        IconSource = Source.User.ProfileImageUrlHttps;
        ScreenName = Source.User.ScreenNameResponse;
        UserName = Source.User.Name;
        CreatedAt = Source.CreatedAt.ToLocalTime();

        Match m = regViaName.Match(Source.Source);
        ViaName = m.Success ? m.Groups[1].Value : Source.Source;

        m = regViaUrl.Match(Source.Source);
        ViaLink = m.Success ? m.Groups[1].Value : string.Empty;
    }

    public string Text { get; }

    public ulong StatusID { get; }

    public string IconSource { get; }

    public string ScreenName { get; }

    public string UserName { get; }

    public DateTime CreatedAt { get; }

    public string ViaName { get; }

    public string ViaLink { get; }

    public void OpenViaLink()
    {
        try {
            System.Diagnostics.Process.Start(ViaLink);
        }
        catch(Win32Exception) {
            throw;
        }
        catch(FileNotFoundException) {
            throw;
        }
    }
}

基本的にViewModelに対応させていますが、viaのURLとクライアント名を分離するためのロジックをコンストラクタにちょろっと入れました。

また、viaのリンクを開くロジックもここに書いています。まあ、Process.Startを呼ぶだけなのですが。
ですが、何かしらの理由で開けないことがあるかと思います。例えばTwitterの仕様変更でURLが含まれなくなったり、WindowsにURLを開くアプリケーションが関連付けられていなかったり。そういう場合はProcess.Startメソッドが例外を吐いてきますが、その処理はModel側でやるべきです(今回はめんどいんで結局再スローしちゃっています)。 「URLが開けませんでした」みたいなエラーメッセージは改めてModel側からイベントを発生させてそれをViewModel経由で表示させるべきですね。ViewModelはあくまでもUIの制約を吸収するためだけの層です。URLが開けなかったというのはUIの制約じゃないですよね。ということは、それをViewModelに押し付けるのは間違っています。

まとめ

今回は初めて連載っぽい記事にしましたがいかがでしたでしょうか。
ここまできて気づいた方も多いかと思いますが、ViewModelとModelは同じようなプロパティやメソッドがたくさん登場してきます。当然です。なぜならば、ViewModelはViewの制約を吸収するための層なので、制約がない部分は素通りさせるべきなのです。なので、素通りする部分にとっては一層余計に挟んでいるような状態になってしまい、同じようなコードが生まれてしまうのです。

プログラマーと言えばコピペコードを嫌う生物ですから、このようなスタイルに懐疑の念を抱く人は多いかと思います。知ったうえで「俺はこっちのほうがいい」と思ったのなら、MVVMになっていないスタイルでプログラムを書いてもいいでしょう。ただし、非同期プログラミングで泣いても私は知りません。

少なくとも、ここまで読んでくださった方たちはMVVMが何かというのはわかってきたかと思います。

MVVMは問題領域の切り分け方です。UIの制約はViewModelに押し付けて、後はすべてModelが引き受ける、それだけです。なので、おそらくはWPFでMVVMを着崩していくよりかは、MVVMの形をキープしながら様々な便利なライブラリなどのアクセサリを身に着けていくのが一番スマートなやり方なのではないかなと私は思います。

MVVMとは何か

MVVMとは何か、それはWPFプログラミングを始めた人は誰しもが気になることでしょう。
私はあまりWindows以外のプラットフォームは触らないのですが、最近はAndroidでもMVVMが流行ってるとかなんとか。

そのせいか、MVVM+適当なワードでググるといくらでもQi○taなどの記事がヒットします。ですが、多くは「データバインディングを使えばMVVM」くらいの浅はかな理解で、MVVMとは何かがまるでわかっていないようです。
まあ「初心者」というのは誰もが通る道ですからそういうのをはなから否定する気はないですが、情報汚染されているのは悲しいので私の理解の範囲を記事にしておこうと思います。

まず忘れてほしいこと

MVVMとは何かというのを調べ始めるとまず見つかることですが、それはMVVMの本質ではないというのをいくつか挙げていきます。私の今回の記事を読むうえでまずは次の3点のことを忘れてください。

1. データバインディングをするのがMVVMである

MVVM初学者は真っ先にこれにぶち当たるかもしれませんが、データバインディングはあくまでもViewとViewModelのデータのやり取りの手段に過ぎません。データバインディングをするのがMVVMではありません。
似たようなものとして「ユーザーインターフェースとビジネスロジックの疎結合を実現するのがMVVMである」というのも大嘘です。

確かにWin32APIやWinFormsなどをいじっていた人からしたら、今までUIとビジネスロジックを一体的に記述していたわけですから、それは大きなモチベーションに見えるでしょう。それぞれを分離して設計できれば、設計を分担するのもやりやすいですし、再利用性、可読性も上がります。何しろ手続き型言語であるC言語やC#などでUIを設計するそれらのフレームワークに比べて、XAMLとかいうマークアップ言語でUIが設計でき、ビジネスロジックをC#で書ければそれはたいそうなメリットに見えるでしょう。私も当初はそうでした。

でも考えてください。それならばView-Modelで良いんです。UIとビジネスロジックを分離するって、それってView-Modelですよね。で、ModelとViewの接続にはデータバインディングを使うと。
実際、この手のモチベーションだけでMVVMを書こうとすると、ViewModelが実質Model的な役割になって、結局のところView-Modelになってしまいます。それじゃあMVVMじゃないんです。

この記事の下のほうへ行くと見えてくると思いますが、MVVMは「UIとビジネスロジックの疎結合」なんていうのは当たり前なんです。それだけだと不都合が起こるから、ViewModelが介在しているんです。

2. MVVMはコードビハインドを書かない

これは結果論としては間違っていないとは思いますが、違います。
何も考えずにコードビハインドを書くとたいていそれがビジネスロジックになりますから、UIとビジネスロジックの分離すらできていない状態になります。それは"論外"以前の問題です。
正確には「WPFはコードビハインドを書かなくて済む」なのです。データバインディング、コマンド、ビヘイビアなどの様々な「Viewとやり取りする方法」があるため、結果としてコードビハインドを書く必要が無いケースがほとんどなのです。

ですので、WPF初心者は「コードビハインドを書かない」をテーマにいろいろ調べてみるといいと思います。いろいろな手段が出てきていい勉強になるでしょう。ですが、あくまでもそれが目的ではないのです。

3. Modelはビジネスロジックなので、UIに依存しない

これは見方によっては間違ってはいないのですが、間違った意味にも取れるので書いておきます。そして、多くの人は間違った意味に取るように思います。
「依存しない」の意味を正しく理解しているかがキーです。

「UIに依存しない」というのは「UIのフレームワークで生まれた制約を知らなくていい」というニュアンスの意味なのです。UIのフレームワークの制約を知らなくていいだけなので、ModelはUIの存在を強く意識します

これだけだと何を言っているのかよくわからないので、メール送信ソフトを作っている場面を想像しながら説明します。
例えば送信ボタンを押したとき、ボタンのコマンドが発動します。コマンドはViewModelに記述しますが、そのViewModel内でTextプロパティやToAddressプロパティ、Subjectプロパティなどの値を取得し、
mail.Send(ToAddress, Subject, Text);
などといったメソッドを呼ぶなどといった処理を記述しがちかと思います。なおこのMailクラスは当然Modelにあるものとします。

これは大間違いなのです。

これだとModelは「ライブラリ」です。このスタイルではView-Model-Libraryになってしまいます。Modelとライブラリの関係なんてUIフレームワークの側からなんかは知ったこっちゃないですよね。ですのでこれはView-Modelと何ら変わらないのです。
なぜこういう書き方をしてしまうかというと、MVVM初心者はViewModelが何のためにあるのかがわかっていないのです。そのため、「ViewとModelをつなぐもの」という説明からViewの範囲をUI、Modelの範囲をビジネスロジックライブラリに限定してしまい、上記のような誤った実装をしてしまうのです。

ではViewModelは何のためにあるのか。実は、ViewModelとはView(=UI)の制約を吸収するためだけの層なのです。なので、Viewがあれば必然的にViewModelも出来上がります。そして残りの領域がすべてModelになるわけです。

「ModelはUIに依存しない」によく似た表現として、「ViewとModelがあって、その間を取り持つのがViewModel」 というのがありますが、これも大いに誤解を生む表現です。正しくは「ViewとViewModelがあって、それ以外の領域がModel」なのです。
「ありき」なのはViewとModelではなく、ViewとViewModelなのです。「Modelはビジネスロジック」といったようにModelの範囲を限定している時点でそれはもう間違っているのです。

WPFの制約

ここからは、UIをWPFに限定してお話をしていきます。
さて、先ほど「UIの制約を吸収するのがViewModel」と言いました。では、WPFにおけるUIの制約とは何なのでしょうか。下記に2例示します。

1. UIは単一スレッドでしか操作できない

WPFはUIを操作できるスレッドは1つしかありません。
複数のスレッドからUIをいじれるようにしようとすると排他制御などで逆に効率が落ちるらしく、それならばと単一スレッドでしか使えないという縛りを入れているようです。
なので、UI以外のスレッドからアクセスしようとしたら例外が吐かれます。

これを吸収するのがViewModelの役目です。
ViewModelは、Modelで何かしらのプロパティの値が変更されたら、それをUIのスレッドでViewに変更通知をします。それ以下のこともそれ以上のこともしません。

MVVMライブラリは、ViewModelの基底クラスでこの処理を行っています。PropertyChangedイベントは必ずUIのスレッドで発生するように作ってあります。ですので初心者は「WPFは単一スレッドでしかUIを触れない」という制約すら知らずに済むのです。MVVMが成功している証です。

ここで注意しなければならないのは、コレクションの変更通知はプロパティ変更通知とはまた別ということです。
ObservableCollectionは何もしなければコレクションを更新したスレッドからそのままCollectionChangedイベントが発生しますので、UIスレッド以外から更新すると例外が飛びます。ですので、ObservableCollectionもコレクションとUIを結びつけるものとしてViewModelとしてUIのスレッドでCollectionChangedイベントを発生させるようなものを作ってやる必要があるのです。
//BindingOperations.EnableCollectionSynchronizationメソッドというものがありこれを使えばUIスレッドに転送できますが、なんでこのような取ってつけたような実装にしたのか理解に苦しみます…。

2. リスト系コントロールは表示されている分しか要素のインスタンスを持たない 

これはどういうことかというと、リストの要素を操作しようとしたときに実体(インスタンス)が無い場合があるということです。
理解するために次のようなコードを書いてみます。

public partial class TestUserControl : UserControl
{
    static int counter = 0;

    public TestUserControl()
    {
        Console.WriteLine(counter++);
        InitializeComponent();
    }
}

まずはこんなコントロールを作ってみます。このコントロールのインスタンスが作られた回数を標準出力に書き出すだけのコントロールです。

<Window x:Class="WpfListControlTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfListControlTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ListBox Name="list">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <local:TestUserControl Height="20" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

こちらがMainWindowです。Listの要素としてTestUserControlを表示しています。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        for(int i = 0; i < 100; i++)
            list.Items.Add(new object());
    }
}


めんどいのでMainWindowのコードビハインドでアイテムを100個追加してみます。

直観的に考えたらソフトを起動したときに100個のTestUserControlが生成されそうな気がします。


14個しかインスタンスが生成されていません。
これは表示されている分で、それ以上のインスタンスは生成されていないのです。

何往復かスクロールしてみます。


100個しかアイテムが無いはずなのに269回インスタンスが作られています。
表示外に外れたインスタンスは消去され、再び表示されたらまたインスタンスから作り直しているんですね。
こうすることで、確かにヒープに負荷はかかりますが何万個とアイテムを持ったリストなどの生成を可能としているのです。

もしもこれにViewModelを用意しなかったらModelでViewをいじろうとしても表示外だったらぬるぽが発生するのです。
そこで、ViewModelでワンクッションを置くことでModelからはいつでもすべての表示項目のViewModelをいじれるようにしてあげます。項目が表示されればViewのインスタンスが生成され、同時にViewModelにバインディングすることであたかも隠れていた項目が今表示されたかのようにふるまうことができるのです。

これもUIの都合ですので、それを吸収するのがViewModelの役目です。

WPFの制約まとめ


どうでしょう、案外WPFにはいろいろな制約があることが分かったと思います。ほかにもいろいろあるかもしれませんが、これだけでMVVMの必要性を説明するのには十分だと思います。

Modelにとっては、UIは単一スレッドでしか触れないことや表示されていない項目のコントロールのインスタンスが存在しないことなんて知ったこっちゃありません。それを吸収するのがViewModelの役割なのです。なので、「これとこれを表示するUI(TextBoxやListViewなど)がある」とか「ボタンが押された」などといったUIの存在自体やUIの主体となる動作はModelは意識する必要があるのです。

このような目的から、Modelから見たViewModelはViewのように見えなければなりません。ViewModelはViewの制約をなくすためのラッパーなのですから、当然機能としてはただのViewと同等になるわけです。

MVVMとは

MVVMの根底には確かにUIとビジネスロジックの分離があります。でも、単純にViewとModelに分けようとすると、WPFの場合はViewに様々な制約があり、Modelがその制約に引きずられたものになってしまいます。そうすると、Modelと言っておきながらWPFに深く入り込んだものが出来上がり、分離とは言い難い形になってしまうのです。
そこで、Viewの制約を吸収してあげる層、すなわちViewModelを作って制約なくModelからViewを操作できるようにしてあげようというのがModel-View-ViewModelこと、MVVMなのです。

概念だけ説明されてもわからないよ、という人もいるかと思いますので、次回は実際にソフトを作りながら解説していきます。

2017年11月13日月曜日

ListView Extensions ver.1.0.0-beta4

beta2に引き続いてbeta4をリリースしました。
えっ?beta3はどこに行ったって?beta2リリース直後にバグを見つけてこっそり差し替えていました。テヘペロッ(๑´ڡ`๑)

ListView Extensions - Nuget

バグフィックスのbeta2,3に比べたら今回はかなり大きなアプデです。バージョン変えようかと思いましたが、1.0.0-beta*という名前である以上、1.0.0をリリースしない限りバージョン変えられないなと思い、結局このままになっています…。
これで安定していたら今度こそ正式リリースするぞ!

今回の変更点

さて、今回のアプデ内容はただ一つにして大きい内容です。コレクションのスレッドセーフ化です。
シングルスレッドが許されるのは小学生までだよねー。

名前は「SyncedSortableObsevableCollection」にしようかと思いましたが、いまどきスレッドセーフじゃないCollectionなんてWPFで使う上では用途が無いと判断し、名前は「SortableObservableCollection」のままとしました。インターフェースは互換性を維持していますので、そのままコンパイルは通るはずです。
一応今までの実装は残していて、同じ名前空間にUnsyncedSortableObservableCollectionとしてObsolate属性を付けています。問題がある方はこっちを使ってください。

なお、スレッドセーフ化する過程で継承構造をより素直な形にしました。以前のバージョンは「SortableCollection」というクラスを継承していましたが、こちらもObsolateになっており、基底クラスは「SyncedObservableCollection」にしております。

SyncedObservableCollection

これは私がフルで実装したクラスで、スレッドセーフな変更通知可能なコレクションです。ReaderWriterLockSlimを使って効率的なロックを行っております。要素技術としては詳しくは前回の記事を見て下さい。

なお、このクラスは非ジェネリックのICollectionインターフェースを継承しているため、IsSynchronizedプロパティSyncRootプロパティを実装しています。コレクションを外部から操作するときに複数回のメソッド呼び出しにわたってlockが必要な場合はこのSyncRootを使ってlockを行ってください。

例えば次のようなケースです。

SortableObservableCollection<Person> People = new SortableObservableCollection<Person>();

//中略

lock(People.SyncRoot)
    People.RemoveAt(People.Count - 1);    //Remove last element

最後の要素を削除する処理ですが、これは
  1. コレクションの長さを取得
  2. コレクションの長さ-1の要素を削除
の2回のコレクションへのアクセスを行っています。
この1.と2.の間に他のスレッドからの書き込みアクセスがあって要素数が増減した場合、「コレクションの長さ-1」が最後の要素のインデックスではなくなってしまいます。これは問題です。

そこで、対策としてSyncRootを使ったlockを行っております。これによって他のスレッドからの書き込みアクセスを阻止しています。
これこそ「ReaderWriterLockSlimを使うべきじゃないの?」と思うかもしれませんが、このようなケースは書き込みアクセスを伴うことが圧倒的に多く、書き込みアクセスをする場合はReaderWriterLockもlock構文も大差がありません。ですので、既存の枠組みで簡単に実現できるこの方法を使うことにしました。
もちろん、コレクションの外でlock構文を使って書き込みをしている間も、中でReaderWriterLockSlimを併用していますので、書き込みのタイミング以外(上記で言えばPeople.Count読み取り時など)は他のスレッドからの読み込みアクセスは許可されます。

ちなみに、本家のObservableCollectionはAddRange/InsertRange/RemoveRangeといった複数の項目を一気に操作する機能は付いていません。しかし、INotifyCollectionChangedインターフェースの機能としてはNotifyCollectionChangedEventArgsクラスを見てもわかりますが複数項目のコレクション操作機能にも対応しています。
なので、このSyncedObservableCollectionではそれらの機能にも対応しています。ループでAddメソッドを複数回呼び出すとその呼び出しごとにイベントが発生しますが、AddRangeだと1回で済むので効率が格段と高まります。

ListViewViewModel

ListViewViewModelは今までもModelをUI以外のスレッドから操作した場合に発生した変更通知をUIのDispatcher経由でViewに反映させる機能を持っていました。ただし、肝心のスレッドセーフ設計は(面倒なので)やっていませんでした。

今回は、このSyncedObservableCollectionを活用してスレッドセーフに設計しています。今までの実装はSortableObservableCollectionと同じくUnsyncedListViewViewModeに移したうえでObsolate属性を付けています。

デッドロックに注意

ここで、ListViewViewModelを使う上での注意を紹介します(私もハマってしばらく原因探しに苦戦していました)。

ListViewViewModelはViewに設置されたボタンなどの入力を受け付けてModelに渡すためのコマンドをいくつか持っています。
例えばRemoveCommandはListViewのうち選択されている項目を削除するコマンドです。Viewのボタン押下を受けてCommandが発火し、Model(SortableObservableCollection)のRemoveメソッドを呼び出します。

気を付けないとここでデッドロックが発生しうるのです。




UIスレッドからのSortableObservableCollectionへのアクセスの直前にその他のスレッドから同じくSortableObservableCollectionに書き込みアクセスがあったとします。
そうすると、先に書き込みアクセスを始めたほうがLockを保持するのでUIスレッドはそのLockが解放されるまで待機します。

しかし、その他のスレッドは変更後に変更通知イベントを発生させ、UIのスレッドでUIの更新作業をしようとします。このときは、すでにUIスレッドはSortableObservableCollectionのLockが解放されるのを待っているのでUIスレッドでのInvokeが行えずデッドロックが発生してしまいます。

AとBの排他制御機構があって、AをLockしたうえでBをLockしようとするスレッドと、BをLockしたうえでAをLockしようとするスレッドがあるとデッドロックが発生する、というのはマルチスレッドプログラミングの基本中の基本です。
今回はUIスレッドとSortableObservableCollectionがそれぞれの排他制御機構となっています。


さてさて、これに対抗する手段もListViewViewModelには追加しております。


UIスレッドが直接SortableObservableCollectionへアクセスしなければよいのです。
UIスレッドからSortableObservableCollectionへのアクセスを行いたいときは別のスレッドに書き込みアクセスを依頼したうえですぐに処理をUIスレッドに戻すようにします。
そうすることで、先にその他のスレッドがSortableObservableCollectionのLockを保持していたとしてもUIスレッドはアイドル状態になっており、その他のスレッドからのUI更新作業もブロックされることはありません。この機構によってデッドロックが回避されるわけです。

ただ、重い作業を並列実行するわけでもないのに別スレッドを起動するので単純に処理が重い上、UI起点の処理をやっているのにその処理が終わる前にUIがいじれるようになるので不都合が起きることもあります。

そこで、これはオプションとし、デフォルトではOFFにしています。
別スレッドからの書き込みとUIスレッドからの書き込みが衝突する可能性がある場合は、ListViewViewModelにてUseNonUIThreadWhenCallingModelプロパティをtrueにしてください。
また、DisableCommandWhenCommandTaskRunningを使うことによってCommand起点のタスクが別スレッドで実行されているときにCommandを無効化することができます。Command起点のタスクが実行中かどうかはCommandTaskRunningプロパティでわかります。


なお、他の注意点として、変更通知イベントを受けたUI更新作業のときに、UIスレッドから直接SortableObservableCollectionに書き込みアクセスをしようとするとやはりデッドロックが起こりますので注意してください。この場合は確実に起きます。
ただ、UI更新作業中にSortableObservableCollectionを書き込むなんておかしな話です。なんでUIの更新作業の名目でデータの変更作業までしてんねんって話です。そんなデッドロックを起こすようなコードを書く人のことは知りません。

ListViewViewModelは同一インスタンスの要素を複数持ってはいけない

これは実は前からある制約です。
ですが、自分自身ですら忘れていたので書いておきます。

SortableObservableCollectionは問題ないのですが、ListViewViewModelは同一インスタンスの要素を複数持てません。
もしも同一インスタンスを追加しようとすると例外(ArgumentException)が発生します。ListViewViewModelのTViewModelが構造体やプリミティブ型の場合は、同値だった場合に例外が発生してしまいます。

なぜこんな仕様になっているかというと、ひとえにSelectedItemsのせいです。

WPFのListViewは複数のアイテムを選択している場合はSelectedItemsでしかその選択されたアイテムをすべて取得することができません。インデックスではなく選択されているアイテムのViewModelインスタンスのコレクションとなります。
この際、複数の同じインスタンスがあった場合、どちらのアイテムが選択されているのか求めることができません。ですので、このような不便な制約があるわけです。


しかし、MVVMモデルをなぞって実装しているのならば、ListViewの各行のアイテムとしてバインディングするのは、通常はSortableObservableCollectionの要素の型に対応するViewModelになるはずです。 なので、SortableObservableCollectionが複数の同一インスタンスを持っていたとしてもViewModelの要素としては別個のインスタンスを作ればいいだけなのです。

これはどのように実現するかというと、ListViewViewModelのコンストラクタで指定します。

public ListViewViewModel(ISortableObservableCollection<TModel> source, Func<TModel, TViewModel> converter, Dispatcher dispatcher);
public ListViewViewModel(ISortableObservableCollection<TModel> source, Func<TModel, TViewModel> converter, Dispatcher dispatcher, DispatcherPriority priority);

ListViewViewModelは2つのコンストラクタを持っています。ですが、Dispatcherの優先度を指定するかデフォルト値(Normal)を採用するかの違いだけで、それ以外は同じです。

sourceはModelのコレクションとなります。SortableObservableCollection<T>を指定する場合が圧倒的に多いでしょう。

問題は2番目の引数です。
このconverterがSortableObservableCollectionで要素が追加されたときに、ViewModelの要素を追加するために呼ばれるデリゲートになります。
ModelをViewModelに変換(ラッピング)するわけですから、ここで

model => new ElementViewModel(model)

のようなラムダ式を渡してやれば十分でしょう。
これでどのようなインスタンスが追加されたときも全自動で新しいインスタンスが生成されていくのでListViewViewModelに同一インスタンスが複数追加されることはありません。

めんどくさがって

model => model

にした場合は例外が飛ぶことがあるので注意してください。


ちなみに、要素がRemoveされたり、別の要素で書き換えられたときは、ElementViewModelがIDisposableを継承していればちゃんとDispose()を呼びますのでご安心を。



さーて、次回こそ正式リリースをするぞ!!!

2017年11月5日日曜日

C#の排他制御 - UpgradeableReadLock

.NET4.0/C#5.0以降、どんどん非同期処理に関する機能が増強されてきています。
特にasync/await構文なんかは、今までは非同期で書こうとすると非常に読みにくいコードになっていたのを、上から下へ流れるようにプログラムを書くだけで非同期処理が実現できてしまうという非常に画期的なものでした。

しかし、そこには非同期処理特有の問題が付きまといます。いくら簡単に非同期処理が書けるからと言って、何も知らずに非同期処理を確実に動かすことはできないのです。

さて、非同期処理で注意しなければならないことの1つとして、「データの整合性」が挙げられます。
非同期処理は複数のスレッドが同時に走るわけですから、それぞれのスレッドから1つのデータにアクセスした場合どのような結果になるか、ということを常に想像しながらコードを書かなければなりません。
そして、必要ならば排他制御を行い、片方のスレッドからデータにアクセスしている場合はもう片方のスレッドがデータにアクセスするのを待ってもらう必要があります。

はじめに - lock構文

C#にはそれに関して便利な構文が用意されています。
ご存知、lock構文です。
Stopwatch sw = new Stopwatch();
sw.Start();

Parallel.ForEach(Enumerable.Range(0, 4), i => {
    Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Start");
    Thread.Sleep(TimeSpan.FromSeconds(1));
    Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Finished");
});

sw.Stop();
まず、こちらがlock構文を使わない場合です。各スレッドで1秒待ってもらっていますが、すべてが並列で実行されるので下記のような実行結果になります(実行結果は環境によって異なります。以下同じ)。

 42ms, 0: Start
 44ms, 2: Start
 44ms, 1: Start
 44ms, 3: Start
1068ms, 0: Finished
1068ms, 1: Finished
1068ms, 2: Finished
1068ms, 3: Finished

基本ですね。

続いてlock構文を入れて、Sleepに入れるのを1つのスレッドのみに絞ってみます。
Stopwatch sw = new Stopwatch();
object lockobj = new object();

sw.Start();

Parallel.ForEach(Enumerable.Range(0, 4), i => {
    lock(lockobj) {
        Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Start");
        Thread.Sleep(TimeSpan.FromSeconds(1));
        Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Finished");
    }
});

sw.Stop();
 41ms, 0: Start
1068ms, 0: Finished
1068ms, 1: Start
2070ms, 1: Finished
2070ms, 2: Start
3073ms, 2: Finished
3073ms, 3: Start
4078ms, 3: Finished

今度は1つのスレッドが確実に終わってから次のスレッドへ移るような実行結果になりました。

今回はlock構文の動作を確かめるための簡単なプログラムですが、通常はデータの読み書きなど複数のスレッドから同時に行われたら都合が悪い部分をlockで囲み排他制御を行うのに使います。

ReadLockとWriteLock

さて、ここまで来て勘のいいひとなら気づいたでしょう。
何でもかんでもデータアクセスにlockをかけるのは決して効率の良いことではないことだと。

データを読み込むだけなら複数のスレッドから同時に行っても問題ないはずです。
ですが、データを読み込んでいる途中に他のスレッドから書き込みが行われたり、データを書き込むときに他のスレッドから書き込みや読み込みを行われては困ります。
//データを書き込みしている途中でも書き込み前後のどちらかの値が取得できさえすればいいなら読み込みを許可してもいいのでは?と思うかもしれませんが、場合によっては書き込み中には書き込み前後のどちらの値でもない値になりうるのです。

lock構文は有無も言わせず排他制御をする構文です。たとえ複数のスレッドがすべて読み込みだけをしたくても同時に行うことを許しません。


.NETにはそれをサポートするためのクラスがあります。

ReaderWriterLockSlim
//名前がいびつですが、実はReaderWriterLockというクラスもあります。このクラスを整理してもっとシンプルにしたものということで、このような名前になっているようです。

機能は上記の通りで、読み取りロックが取得されているときは他のスレッドからも読み取りロックを取得することができますが書き込みロックは取得できません。書き込みロック取得中は読み取り/書き込みロック両方とも取得できません。
ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
Stopwatch sw = new Stopwatch();
sw.Start();

Parallel.ForEach(Enumerable.Range(0, 12), i => {
    if(i % 2 == 0) {
        try {
            rwlock.EnterReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterReadLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitReadLock");
            rwlock.ExitReadLock();
        }
    } else if(i % 2 == 1) {
        try {
            rwlock.EnterWriteLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterWriteLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitWriteLock");
            rwlock.ExitWriteLock();
        }
    }

});

sw.Stop();
一気にインデントが多くなって見にくくなってしまいました。Lockは取得したら確実に解放しなければならないので、try-finally構文を使っているためです。
//lock構文も内部的にはMonitorクラスを使ったtry-finally文に展開されます。

これを実行すると次のような実行結果になります。

 70ms, 0: EnterReadLock
 71ms, 6: EnterReadLock
 71ms, 4: EnterReadLock
 71ms, 2: EnterReadLock
 95ms, 8: EnterReadLock
1071ms, 0: ExitReadLock
1095ms, 6: ExitReadLock
1095ms, 4: ExitReadLock
1096ms, 8: ExitReadLock
1096ms, 2: ExitReadLock
1096ms, 11: EnterWriteLock
2096ms, 11: ExitWriteLock
2097ms, 7: EnterWriteLock
3098ms, 7: ExitWriteLock
3098ms, 1: EnterWriteLock
4099ms, 1: ExitWriteLock
4099ms, 5: EnterWriteLock
5099ms, 5: ExitWriteLock
5099ms, 3: EnterWriteLock
6100ms, 3: ExitWriteLock
6100ms, 9: EnterWriteLock
7101ms, 9: ExitWriteLock
7101ms, 10: EnterReadLock
8102ms, 10: ExitReadLock


最初は5つのスレッドが同時にReadLockに入っていますが、WriteLockはおそらくブロックされていることがわかります。そして、ReadLockがすべて解放されると待っていたWriteLockに入っています。WriteLockは1つずつ入っており、10番のReadLockもブロックされていることが分かります。

lock構文のような便利な構文が無いのでtry-finallyの冗長さが目立ってしまう書き方ですが、単純なlockより効率的なデータアクセスが見込めるでしょう。

UpgradeableReadLock

さて、話がどんどん大きくなっていきます。

例えばスレッドセーフなDictionaryを設計しているとき、新たな値を渡されたらどういう処理になるでしょうか。
  1. すでにキーが存在するか確かめる
  2. キーが存在しなければキーと値を追加する
  3. キーが存在すればそのキーの値が同じか確かめる
  4. 値が違っていたら値を書き換える
という流れになるかと思います。
ここで、青色で書いたのは読み取りで、赤色で書いたのは書き込みです。
じゃあこれを実装するときは、青の部分のコードはReadLockを掛けて、赤の部分のときはWriteLockを掛ければいいのかと言えば、答えはNoです。

ReadLockをExitしてからWriteLockを取得するまでの間に別のスレッドがWriteLockを取得して値を書き換えたらどうなるでしょう。1.をやって例えばNoと判定したのに、その直後に別スレッドがそのキーを追加していたら2.を実行するときに意図していない状態になってしまいます。
かといって、ReadLockを取得したままWriteLockを取ろうとするとデッドロックを起こします。他のスレッドもReadLock取得中にWriteLockへ昇格しようとしていた場合、互いに他のスレッドのReadLockが解放されるのを待ってしまい、どちらのスレッドもそこから先へ進めなくなってしまうのです。

じゃあどうするか、一つの答えは全体をWriteLockにすれば良いでしょう。ですが、1.や3.を行っている間は別スレッドが読み取り操作をするだけなら許されるべきです。全体をWriteLockにしてしまうとそれすら許されなくなってしまいます。lock構文は条件が厳しすぎて効率が悪いからReadLockとWriteLockに分けようという話になったのに、結局それが生かせなくなってしまいます。


そこで登場するのがUpgradeableReadLockです。
「WriteLockに昇格予定があるReadLock」は唯一にすることで、昇格時にデッドロックを起こすのを防ぎます。逆に言えば、ただのReadLockは昇格予定の無いReadLockということになります。

具体的には、下表のような動きをします。
現在のLock\取得しようとしているLockReadLockUpgradeableReadLockWriteLock
なし
ReadLock×
UpgradeableReadLock××
WriteLock×××

○がブロックされない(取得できる)Lock、×がブロックされるLockということになります。

実際に動作を試してみましょう。
ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
Stopwatch sw = new Stopwatch();
sw.Start();

Parallel.ForEach(Enumerable.Range(0, 12), i => {
    if(i % 3 == 0) {
        try {
            rwlock.EnterUpgradeableReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterUpgradeableReadLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitUpgradeableReadLock");
            rwlock.ExitUpgradeableReadLock();
        }
    } else if(i % 3 == 1) {
        try {
            rwlock.EnterReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterReadLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitReadLock");
            rwlock.ExitReadLock();
        }
    } else if(i % 3 == 2) {
        try {
            rwlock.EnterWriteLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterWriteLock");

            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitWriteLock");
            rwlock.ExitWriteLock();
        }
    }

});

sw.Stop();
 59ms, 0: EnterUpgradeableReadLock
 59ms, 7: EnterReadLock
 59ms, 1: EnterReadLock
 59ms, 4: EnterReadLock
1059ms, 0: ExitUpgradeableReadLock
1060ms, 4: ExitReadLock
1060ms, 1: ExitReadLock
1060ms, 7: ExitReadLock
1061ms, 8: EnterWriteLock
2062ms, 8: ExitWriteLock
2063ms, 11: EnterWriteLock
3064ms, 11: ExitWriteLock
3064ms, 5: EnterWriteLock
4065ms, 5: ExitWriteLock
4065ms, 2: EnterWriteLock
5066ms, 2: ExitWriteLock
5066ms, 10: EnterReadLock
5066ms, 6: EnterUpgradeableReadLock
6068ms, 10: ExitReadLock
6069ms, 6: ExitUpgradeableReadLock
6069ms, 3: EnterUpgradeableReadLock
7069ms, 3: ExitUpgradeableReadLock
7069ms, 9: EnterUpgradeableReadLock
8070ms, 9: ExitUpgradeableReadLock

UpgradeableReadLock中はReadLockは許されていますが、WriteLockや他のUpgradeableReadLockはブロックされているであろうことがわかります。


ちなみに、UpgradeableReadLock中にWriteLockに昇格するには単にEnterWriteLock()~ExitWriteLock()の一連の構文を入れればいいだけです。
ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
Stopwatch sw = new Stopwatch();
sw.Start();

Parallel.ForEach(Enumerable.Range(0, 20), i => {
    if(i % 10 == 0) {
        try {
            rwlock.EnterUpgradeableReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterUpgradeableReadLock");
            //Thread.Sleep(TimeSpan.FromSeconds(1));
            try {
                rwlock.EnterWriteLock();
                Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Upgraded");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
            finally {
                Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: Downgraded");
                rwlock.ExitWriteLock();
            }
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitUpgradeableReadLock");
            rwlock.ExitUpgradeableReadLock();
        }
    } else {
        try {
            rwlock.EnterReadLock();
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: EnterReadLock");
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        finally {
            Console.WriteLine($"{sw.ElapsedMilliseconds,4}ms, {i,2}: ExitReadLock");
            rwlock.ExitReadLock();
        }
    }
});

sw.Stop();
 49ms, 3: EnterReadLock
 49ms, 1: EnterReadLock
 49ms, 2: EnterReadLock
 49ms, 4: EnterReadLock
 49ms, 0: EnterUpgradeableReadLock
 83ms, 5: EnterReadLock
 86ms, 6: EnterReadLock
 86ms, 7: EnterReadLock
 88ms, 8: EnterReadLock
1050ms, 3: ExitReadLock
1084ms, 1: ExitReadLock
1084ms, 2: ExitReadLock
1085ms, 4: ExitReadLock
1085ms, 5: ExitReadLock
1087ms, 7: ExitReadLock
1087ms, 6: ExitReadLock
1088ms, 8: ExitReadLock
1088ms, 0: Upgraded
2089ms, 0: Downgraded
2089ms, 17: EnterReadLock
2089ms, 15: EnterReadLock
2089ms, 13: EnterReadLock
2089ms, 12: EnterReadLock
2089ms, 14: EnterReadLock
2089ms, 18: EnterReadLock
2089ms, 11: EnterReadLock
2089ms, 16: EnterReadLock
2089ms, 9: EnterReadLock
3037ms, 19: EnterReadLock
3089ms, 0: ExitUpgradeableReadLock
3089ms, 10: EnterUpgradeableReadLock
3092ms, 15: ExitReadLock
3092ms, 17: ExitReadLock
3093ms, 12: ExitReadLock
3093ms, 14: ExitReadLock
3094ms, 11: ExitReadLock
3093ms, 13: ExitReadLock
3095ms, 16: ExitReadLock
3095ms, 9: ExitReadLock
3093ms, 18: ExitReadLock
4037ms, 19: ExitReadLock
4037ms, 10: Upgraded
5038ms, 10: Downgraded
6038ms, 10: ExitUpgradeableReadLock

UpgradeableReadLockからWriteLockに昇格している間は他スレッドがReadLockを取得するのをブロックされていますが、WriteLock終了後(降格後)はUpgradableReadLockをExitしていなくても再びReadLockが取れるようになっていることがわかります。


この節の最初で「UpgradeableReadLockはスレッドセーフなDictionaryを作るのに使える」と言いましたが、多分Dictionary程度だとこの程度の効率化はたかが知れています。
しかし、MVVMなどのフレームワークにおいては大きく効果が出ます。値の変更後に逐一イベントが飛んで、イベントでいろいろな更新作業を行います。 その更新作業中ずっとWriteLockにして他のReadアクセスをブロックしていたら効率がかなり落ちてしまいます。このUpgradeableReadLockそのような場面で威力を発揮する排他制御手法となるでしょう。

2017年10月24日火曜日

ListView Extensions ver.1.0.0-beta2

最近久しぶりにWPFを触ったのですが、その中でListViewを使う機会がありました。

身近なListViewと言えばWindowsのエクスプローラーですが、例えばこれはヘッダーをクリックすることで思い思いの要素でファイルのソートができるのに対して、WPFのListViewにはそのような基本機能が備えられていません。
とは言っても、備えるのもかなりの根気がいる作業ですし、そもそも元のコレクションに介入する作業はPresentation Foundationと名の付くフレームワークがやるべきことではない気がします。

そこで、ListView Extensionsです。

MVVMスタイルの形をしながら、ListViewでの基本機能が多数取り揃えられているライブラリです。
例えば、ListViewのソート、選択項目に対する移動、削除などです。
このような機能を実現するにあたって、View⇔ViewModel⇔Modelのすべての領域にわたって素晴らしいクラスが提供されています。

完全に自画自賛です。


ところで、バグがありました。

ListViewViewModelでClear()を呼び出したとき、より正確にはRemoveなどで最後の1項目を削除したとき、InvalidOperationExceptionが発生することがあるバグがありました。

MoveUpSelectedItemCommand/MoveDownSelectedItemCommandの実装にて、このコマンドが有効になる条件として「選択されているアイテムの個数が1以上、かつリストの先頭/最後尾のアイテムが選択されていない」というロジックを組んでいました。
しかし、 最後の1つの項目を削除したときにSelectedItemsの反映がリストのコレクションの反映より遅くなることがあるようで、その場合、「選択されているアイテムが1個あるがリストのコレクションのアイテムは0個」という状況が起こってしまうようです。その時に、1つ目の条件をすり抜けて2つ目の条件の判定に入った際、リストの先頭または最後尾のアイテムを取得しようとしてInvalidOperationExceptionがスローされているようでした。

このバグは、「リストのコレクションの数が1個以上」という条件を追加することによって回避しました。


あと、自分で作っておいて少しはまったところですが、このライブラリ、UI以外のスレッドからアイテムの操作をしてもUIに正常に反映される仕組み(Dispatcher経由でのアクセス)に対応しているにもかかわらず、スレッドセーフに作られていません。
気が向いたらSorableSynchronizedObservableCollectionなどを作るかもしれませんが、それまでは皆さん自分でlockなどをして使ってくださいね…。

とりあえず、例の最後の1個を消したら例外を吐くバグを直したものをbeta2としてうpしておきました。
ListViewExtensions 1.0.0-beta2
細かな使い方はbeta1をリリースしたときの記事を確認してください。

いつになったらプレリリース外そうかな…。 

2017年8月19日土曜日

BME280をPIC32MX250F128B+Harmonyで使う

前回に引き続き、I2CセンサをPIC32で扱うお話です。

BME280はBOSCH製の温湿度・気圧センサです。
個人的にはBOSCHと言えばドイツの自動車部品メーカーというイメージですが、こういった半導体部品も作ってるんですね。驚きです。

さてさて、こいつもI2C/SPI両対応のチップで、分解能が温度0.01℃、湿度0.008%、気圧0.18Paとかなり細かくデータを取ることができます。データシートもドイツメーカーだからか英語が非常にシンプルで読みやすく、内容もコンパクトにまとめられています。

このセンサの使用方法としては
  1. 設定の書き込み
  2. Trimming Parameterの読み込み
  3. (生の)測定データの読み込み
  4. 測定データの演算
の流れとなっています。
Trimming ParameterはCalibration Dataとも書かれており、生の測定値を℃、%、hPaに変換するにあたって使われる係数です。
センサによって個体差があるので、メーカー出荷時にその個体差に合わせて係数を設定しておき、ユーザーは生の測定データをTrimming Parameterを使って演算することで正確な温湿度や気圧を得ることができます。

順に追って見ていきましょう。

void I2CEventHandler(DRV_I2C_BUFFER_EVENT event, DRV_I2C_BUFFER_HANDLE bufferHandle, uintptr_t context)
{
    /*
    switch (event)
    {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            break;
        case DRV_I2C_BUFFER_EVENT_ERROR:
            break;
        default:
            break;
    }
    */
}

_Bool I2C_InitAndOpen(I2C_HANDLE *pobj, SYS_MODULE_INDEX index)
{
    pobj->hDriver = DRV_I2C_Open(index, DRV_IO_INTENT_READWRITE);
    pobj->hBufferDriver = NULL;
    
    if(pobj->hDriver == (DRV_HANDLE)NULL)
        return false;
    
    DRV_I2C_BufferEventHandlerSet(pobj->hDriver, I2CEventHandler, (uintptr_t)pobj);

    return true;
}

DRV_I2C_BUFFER_EVENT I2C_GetTransferStatus(I2C_HANDLE *pobj)
{
    return DRV_I2C_TransferStatusGet(pobj->hDriver, pobj->hBufferDriver);
}

_Bool I2C_IsCommunicating(I2C_HANDLE *pobj)
{
    if(pobj->hBufferDriver == NULL)
        return false;
    else {
        DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj);        
        return (event != DRV_I2C_BUFFER_EVENT_COMPLETE) && (event != DRV_I2C_BUFFER_EVENT_ERROR);
    }
}

uint32_t I2C_GetTransferredBytes(I2C_HANDLE *pobj)
{
    return DRV_I2C_BytesTransferred(pobj->hDriver, pobj->hBufferDriver);
}

まずはI2C周りのコードです。I2CバスをBME280とMPU-9250で共有しているので、共通するI2C関係のプログラムを分離しています。

#define BME280_SENDDATA_COUNT_MAX  4

typedef struct {
    I2C_HANDLE *pHandle;
    uint8_t WriteBuffer[BME280_SENDDATA_COUNT_MAX * 2];
    uint8_t SlaveAddress;
    
    uint16_t dig_T1;
    int16_t dig_T2;
    int16_t dig_T3;
    uint16_t dig_P1;
    int16_t dig_P2;
    int16_t dig_P3;
    int16_t dig_P4;
    int16_t dig_P5;
    int16_t dig_P6;
    int16_t dig_P7;
    int16_t dig_P8;
    int16_t dig_P9;
    uint8_t dig_H1;    
    int16_t dig_H2;    
    uint8_t dig_H3;    
    int16_t dig_H4;    
    int16_t dig_H5;    
    int8_t dig_H6;    
} BME280;

_Bool BME280_Init(BME280 *pobj, I2C_HANDLE *pHandle, uint8_t SlaveAddr)
{
    pobj->SlaveAddress = SlaveAddr;
    pobj->pHandle = pHandle;

    return true;
}

そのためBME280の初期化関数はアドレスの登録だけになっています。
ちなみにBME280のデータを保持する構造体にはdig_**みたいな変数をたくさん用意してありますがこれらがTrimming Parametersです。配列にしたほうがカッコイイ気もしますが、データシート等に倣ってこう書いておくことにします。

_Bool BME280_StartWritingResistor(BME280 *pobj, uint8_t address, uint8_t data)
{
    BME280_DATA_ADDRESS_PAIR pair;
    
    pair.Address = address;
    pair.Data = data;
    
    return BME280_StartBurstWritingResistor(pobj, &pair, 1);
}

_Bool BME280_StartBurstWritingResistor(BME280 *pobj, BME280_DATA_ADDRESS_PAIR *pairs, int count)
{
    int i;
    
    if(count <= 0 || count > BME280_SENDDATA_COUNT_MAX || I2C_IsCommunicating(pobj->pHandle))
        return false;
    
    for(i = 0; i < count; i++) {
        pobj->WriteBuffer[i * 2 + 0] = pairs[i].Address;
        pobj->WriteBuffer[i * 2 + 1] = pairs[i].Data;
    }
    
    pobj->pHandle->hBufferDriver = DRV_I2C_Transmit(pobj->pHandle->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, count * 2, NULL);
    
    return pobj->pHandle->hBufferDriver != NULL;
}

_Bool BME280_StartReadingResistor(BME280 *pobj, uint8_t address, uint8_t *pdata)
{
    return BME280_StartBurstReadingResistor(pobj, address, pdata, 1);
}

_Bool BME280_StartBurstReadingResistor(BME280 *pobj, uint8_t address, uint8_t *pdata, int length)
{
    if(I2C_IsCommunicating(pobj->pHandle))
        return false;
    
    pobj->WriteBuffer[0] = address;

    pobj->pHandle->hBufferDriver = DRV_I2C_TransmitThenReceive(pobj->pHandle->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 1, pdata, length, NULL);
    
    return pobj->pHandle->hBufferDriver != NULL;
}

送受信関係のコードです。
基本的には、書きこみの場合はレジスタアドレスを送ってからデータ内容を送信、読み込みの場合はレジスタアドレスを送ってから読み込みモードでリスタートして受信という形になります。

ただ、複数レジスタを同時に読み書きするburst write / readは読み込むときと書きこむときで仕様が違います。
読み込みの場合はレジスタが自動的にインクリメントされるので、読み込みたいアドレスの先頭のアドレスを指定してやって読みたいバイト数分のデータを連続で読み込めば良いです。
書き込みの場合は自動的にインクリメントはされません。その代わり、「レジスタアドレス+書きこむ値」の計2バイトをセットで送る形になります。
読み込みは測定値のレジスタアドレスが連番になっていますから自動インクリメントが助かりますが、書きこみの場合は設定になるので連番とは限らず、このような書きこみ方ができるとかえって助かります。

ところで、湿度は16bit、温度と気圧は20bit値なわけで、2バイトないしは3バイトで1つのデータとなっています。この手の複数バイトを1バイトずつ分割して転送する処理をするときは「最初のバイトを送信してから最後のバイトを送信し終わるまでの間に値が更新されたらどうなるのか?」という疑問が付きまといます。
MPU-9250のときも気になってデータシートを探したのですが、特にそのようなことは書かれていませんでした。 ですが、このBME280にはそれに関する記述がありました。
In normal mode, the timing of measurements is not necessarily synchronized to the readout by the user. This means that new measurement results may become available while the user is reading the results from the previous measurement. In this case, shadowing is performed in order to guarantee data consistency. Shadowing will only work if all data registers are read in a single burst read. Therefore, the user must use burst reads if he does not synchronize data readout with the measurement cycle. Using several independent read commands may result in inconsistent data.
If a new measurement is finished and the data registers are still being read, the new measurement results are transferred into shadow data registers. The content of shadow registers is transferred into data registers as soon as the user ends the burst read, even if not all data registers were read.
The end of the burst read is marked by the rising edge of CSB pin in SPI case or by the recognition of a stop condition in I2C case. After the end of the burst read, all user data registers are updated at once. (データシート4.1章「Data register shadowing」より引用)
(拙訳)ノーマルモードでは、測定のタイミングはデータの読み出しと同期されるとは限りません。これはすなわち、ひとつ前の測定値を読み出しているときに新たな測定が完了する可能性があるということを意味しています。そのような場合は、シャドーイングがデータの整合性を保証します。シャドーイングはデータレジスタを1回のバーストリードで読み出しているときのみ機能します。そのため、データの測定サイクルに同期せずに読み出す場合は必ずバーストリードを行う必要があります。個々のレジスタを個別に読みだすとデータの整合性が崩れる可能性があります。
測定が終了したときがレジスタの読み出しをしている最中だった場合、その新しい測定値はシャドーレジスタに転送されます。バーストリードが終了したタイミングでシャドーレジスタの値はデータレジスタに転送されるため、他のレジスタをこれから読み出す予定だったとしてもそれは感知されません。
バーストリードの終了は、SPI接続の場合はCSBピンの立ち上がりエッジ、I2C接続の場合はストップビットで判断されます。バーストリードの終了時に、すべてのデータレジスタは一度に更新されます。
英文を読んでいると何度も同じようなことを言っていて混乱してきますが、要するに、バーストリード中に測定が完了した場合、新しい測定値はシャドーレジスタ(本来読み出すレジスタに書き込みができないときに、いったん測定データを保持しておくためのレジスタ。本来のレジスタの影のような存在であるため、このような名前になっている。)に一旦保存され、バーストリードの終了とともに読み出し用レジスタに転送されます。このため、データの整合性は保証されるということです。
データの整合性とは、マルチバイトデータのそれぞれのバイトが単一の測定から得られたものか、という意味以外にも、温度データと気圧データが同じ時刻に測定されたものである、という意味まで含んでいると思われます。ですので、原則として、このセンサではすべての測定値を同時にバーストリードする必要があります。


さて、今回は設定をこのようにしました。

BME280_RV_CONFIG_t config;
BME280_RV_CTRL_HUM_t ctrl_hum;
BME280_RV_CTRL_MEAS_t ctrl_meas;
BME280_DATA_ADDRESS_PAIR data[3];

ctrl_hum.OSRS_H = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.OSRS_P = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.OSRS_T = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.MODE = BME280_VAL_MODE_NORMAL;
config.T_SB = BME280_VAL_TIME_STANDBY_0_5;
config.FILTER = BME280_VAL_FILTER_OFF;
config.SPI3W_EN = 0;

data[0].Address = BME280_RA_CTRL_HUM;
data[0].Data = ctrl_hum.Value;
data[1].Address = BME280_RA_CTRL_MEAS;
data[1].Data = ctrl_meas.Value;
data[2].Address = BME280_RA_CONFIG;
data[2].Data = config.Value;

if(BME280_StartBurstWritingResistor(&appData.bme280, data, 3))
    appData.state = APP_STATE_BME280_CHECK_CONFIG;
else {
    strcpy(szErrorMessage, "ERROR on APP_STATE_BME280_WRITE_CONFIG\r\n");
    appData.state = APP_STATE_ERROR;
}
break;

各設定用レジスタの値はビットフィールド構造体を作ってあげて、マクロでわかりやすく設定できるようにしました。
同じ物理量を複数回測定することで精度を出す手法「オーバーサンプリング」を使うことができますが、その分サンプリング周波数が落ちるので今回は使わないことにしています。
モードはスリープ(SLEEP)、1回のみ測定(FORCE)、連続で測定(NORMAL)の3種類から選べます。今回はもちろんNORMALです。
また、測定をしてから次の測定までのインターバルを設定することができます。今回は最短の0.5msにしましたが、あくまでもそれはインターバルで、測定周期とは異なることに注意が必要です。この辺の時間の見積もり方はデータシートに詳しく載っています。

さて、設定をしたら次はTrimming Parameterを読み込まなくてはいけません。
レジスタはアドレス0x88~0xA1の26バイトと、0xE1~0xF0の16バイトに分かれて格納されていますが、このうち使うのは前者26バイト分と後者7バイト分のようです。
かなり不規則なデータの入れ方をしていますが、格納するときはこのようなプログラムになります。

DRV_I2C_BUFFER_EVENT BME280_CheckAndSetTrimingParam_Low(BME280 *pobj, uint8_t *pdata)
{
    DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj->pHandle);
    if(event == DRV_I2C_BUFFER_EVENT_COMPLETE) {
        pobj->dig_T1 = ((uint16_t)pdata[ 1] << 8) | pdata[ 0];
        pobj->dig_T2 = ((uint16_t)pdata[ 3] << 8) | pdata[ 2];
        pobj->dig_T3 = ((uint16_t)pdata[ 5] << 8) | pdata[ 4];
        pobj->dig_P1 = ((uint16_t)pdata[ 7] << 8) | pdata[ 6];
        pobj->dig_P2 = ((uint16_t)pdata[ 9] << 8) | pdata[ 8];
        pobj->dig_P3 = ((uint16_t)pdata[11] << 8) | pdata[10];
        pobj->dig_P4 = ((uint16_t)pdata[13] << 8) | pdata[12];
        pobj->dig_P5 = ((uint16_t)pdata[15] << 8) | pdata[14];
        pobj->dig_P6 = ((uint16_t)pdata[17] << 8) | pdata[16];
        pobj->dig_P7 = ((uint16_t)pdata[19] << 8) | pdata[18];
        pobj->dig_P8 = ((uint16_t)pdata[21] << 8) | pdata[20];
        pobj->dig_P9 = ((uint16_t)pdata[23] << 8) | pdata[22];
        pobj->dig_H1 =            pdata[25];
    }
    return event;
}

DRV_I2C_BUFFER_EVENT BME280_CheckAndSetTrimingParam_High(BME280 *pobj, uint8_t *pdata)
{
    DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj->pHandle);
    if(event == DRV_I2C_BUFFER_EVENT_COMPLETE) {
        pobj->dig_H2 = ((uint16_t)pdata[1] << 8) |   pdata[0];
        pobj->dig_H3 =                               pdata[2];
        pobj->dig_H4 = ((uint16_t)pdata[3] << 4) | ( pdata[4]       & 0x0F);
        pobj->dig_H5 = ((uint16_t)pdata[5] << 4) | ((pdata[4] >> 4) & 0x0F);
        pobj->dig_H6 =                               pdata[6];
    }
    return event;
}

前半部分で偶数バイトは下位、奇数バイトは上位としているのかと思いきやdig_H1だけ違ったり、後半部分では4ビットずつ使うところなんかが出てきていささか気持ち悪いですが、とりあえずこんな感じのコードになりました。


さて、今度はこれを使って生の測定値を変換することになりますが、これまた非常にわかりにくい処理になっています。

データシートを見ると、コードが書いてあるだけで、具体的にどのような数式で処理をしているかはっきりしません。変数もval1などと言った意味のない名前で、使いまわしもされており、完全に人に読ませるようなコードではありません。
もっと言えば、データシートでは符号付き整数型のシフト演算がされており、負の値に対するシフト演算はC言語では未定義(処理系依存)の動作となります。そのうえ、データシートに「値の変換はBosch Sensortecから出ているAPIを使うことを推奨します」と書かれている始末です。じゃあなんのためにデータシートにこんなわけわからんコード載せてんねん。
Bosch Sensortec BME280 sensor driver
これが例のAPIです。
見るとデータシートのコードから修正され、除算演算子を使った計算に置き換えられています。
実際は2^nの除算しか使ってないので、コンパイラの腕の見せ所(最適化でシフト処理に置き換えられるか)ですが、さすがに処理系依存のコードを書くわけにもいかないのでこの辺が妥当でしょう。

生の測定値とはいったい何を表している値なのか、この演算処理はどういった計算をしているのかはデータシートでの説明が皆無ですので置いておくとして、ここで重要なことは気圧や湿度の変換には温度データが必要な点です。上記APIには「t_fine」という変数に温度が保存され、気圧、湿度の変換関数で使われていますが、このt_fineには温度[℃]の値が5120倍された数値になっていて保存されています。
その点に注意しながら上記APIのコードを使いましょう。

これで無事測定ができるようになりました。


これは、外出から帰ってきたときからの部屋の温湿度・気圧の推移です。最初は32℃くらい室温がありましたが、エアコンを入れたので28℃くらいまで下がっている様子がわかります。湿度もエアコンによって下がっていますね。


今回はBME280に絞った記事ですが、実際は先日のMPU-9250と同時にデータを取っています。
もうちょっとプログラムに磨きを掛けて、細かな完成度を上げていきたいです。

2017年8月14日月曜日

MPU-9250をPIC32MX250F128B+Harmonyで使う

前回の記事でUSBメモリーの読み書きをやりました。
さて、なぜこれをやったかというと、ロガー的なものを作ってみたかったからなんですね。

今回は、9軸モーションセンサであるInvenSense製MPU-9250をいじってみようと思います。

MPU-9250はジャイロセンサ、加速度センサ、地磁気センサの3つのセンサが搭載されているセンサで、I2CまたはSPIでその値を読みだすことができます。
ほかにもDMP(Digital Motion Processor)という計算チップが入っており、これら3つのセンサの値をから姿勢を算出し、オイラー角や四元数などの値を吐き出すこともできるようです。

今回は、とりあえずとっかかりとしてI2C接続でセンサの吐き出す加速度と角速度、そしておまけで付いてくる温度を読み出してUSBメモリーに記録していきたいと思います。

ちなみに、なぜ地磁気センサを読み出さないかというと、MPU-9250の中で別チップになっているからです。MPU-9250の中には加速度センサやジャイロセンサとは独立して、完全に別チップ(旭化成製AK8963)として地磁気センサが入っています。そのため、加速度センサの値等とは完全に別枠で制御してやる必要があります。なので、これはまた今度の課題とします。


さて、細かなスペックはデータシートレジスターマップを見ればだいたいわかります。
ですので、どんどんコードを書いていきたいと思います。

typedef struct {
    DRV_HANDLE hDriver;
    DRV_I2C_BUFFER_HANDLE hBufferDriver;
    uint8_t WriteBuffer[2];    
    uint8_t SlaveAddress;
} MPU9250;

まずは、MPU-9250にアクセスする用のデータをストックする構造体を作っておきます。

void I2CEventHandler(DRV_I2C_BUFFER_EVENT event, DRV_I2C_BUFFER_HANDLE bufferHandle, uintptr_t context)
{
    /*
    switch (event)
    {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            break;
        case DRV_I2C_BUFFER_EVENT_ERROR:
            break;
        default:
            break;
    }
    */
}

_Bool MPU9250_Init(MPU9250 *pobj, SYS_MODULE_INDEX index, uint8_t SlaveAddr)
{
    pobj->SlaveAddress = SlaveAddr;
    
    pobj->hDriver = DRV_I2C_Open(index, DRV_IO_INTENT_READWRITE);
    pobj->hBufferDriver = NULL;
    DRV_I2C_BufferEventHandlerSet(pobj->hDriver, I2CEventHandler, (uintptr_t)pobj);

    return pobj->hDriver != (DRV_HANDLE)NULL;
}

次は初期化の関数です。特に変なところは無いと思います。
I2Cの送受信が終わったときなどにそのイベントを受け取る関数を一応用意していますが、今回は使わないので中身は空にしています。

DRV_I2C_BUFFER_EVENT MPU9250_GetTransferStatus(MPU9250 *pobj)
{
    return DRV_I2C_TransferStatusGet(pobj->hDriver, pobj->hBufferDriver);
}

_Bool MPU9250_IsI2CCommunicating(MPU9250 *pobj)
{
    if(pobj->hBufferDriver == NULL)
        return false;
    else {
        DRV_I2C_BUFFER_EVENT event = MPU9250_GetTransferStatus(pobj);        
        return (event != DRV_I2C_BUFFER_EVENT_COMPLETE) && (event != DRV_I2C_BUFFER_EVENT_ERROR);
    }
}

I2Cの状態を確認するラッパー関数です。
MPU9250_IsI2CCommunicating関数は、これがTRUEを返すと現在送受信中で、FALSEを返すと現在アイドル中であることを表します。

_Bool MPU9250_StartWritingResistor(MPU9250 *pobj, uint8_t address, uint8_t data)
{
    if(MPU9250_IsI2CCommunicating(pobj))
        return false;
    
    pobj->WriteBuffer[0] = address;
    pobj->WriteBuffer[1] = data;
    
    pobj->hBufferDriver = DRV_I2C_Transmit(pobj->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 2, NULL);
    
    return pobj->hBufferDriver != NULL;
}

_Bool MPU9250_StartReadingResistor(MPU9250 *pobj, uint8_t address, uint8_t *pdata)
{
    return MPU9250_StartBurstReadingResistor(pobj, address, pdata, 1);
}

_Bool MPU9250_StartBurstReadingResistor(MPU9250 *pobj, uint8_t address, uint8_t *pdata, int length)
{
    if(MPU9250_IsI2CCommunicating(pobj))
        return false;
    
    pobj->WriteBuffer[0] = address;

    pobj->hBufferDriver = DRV_I2C_TransmitThenReceive(pobj->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 1, pdata, length, NULL);
    
    return pobj->hBufferDriver != NULL;
}

これが実際に送受信をする関数です。

注意しなければならないことは、スレーブアドレスはそのまま入れられないということです。
I2Cは最初の8bit中上位7bitでスレーブアドレスを送り、下位1bitでリードモードかライトモードかを指定します。
Harmonyのドライバ関数では、そのリード/ライトのビット付加は自動でやってくれますが、アドレスの1bitシフトはやってくれません。ですので、スレーブアドレスは左に1bitシフトしたうえで渡す必要があります。

また、これらの関数は読み書きをしたいメモリアドレスをパラメーターとして受け取りますが、DRV_I2C_TransmitThenReceive関数などでは内部でこの送信内容のデータのポインタを保持しています。ですので、この関数から制御が戻ると値が不定となるスタック領域ではだめで、MPU9250構造体の中に確保したメモリにアドレスをコピーしてそれを参照するようにしています。

uint16_t MPU9250_ParseUInt16(uint8_t *pData)
{
    return (uint16_t)pData[0] << 8 | pData[1];
}

int16_t MPU9250_ParseInt16(uint8_t *pData)
{
    return (int16_t)MPU9250_ParseUInt16(pData);
}

void MPU9250_ParseVector(uint8_t *pData, VECTOR_3D *pOut)
{
    pOut->X = MPU9250_ParseInt16(pData);
    pOut->Y = MPU9250_ParseInt16(pData + 2);
    pOut->Z = MPU9250_ParseInt16(pData + 4);
}

あとはユーティリティ的なものですが、MPU-9250は16bit値は上位8bitのほうが若番のアドレスが振られています。多くの処理系とは異なる(とか言うと怒られそうですが)ので、手動でシフト演算するように作っています。

さて、このライブラリを使う側(app.c)は、だいたいこんな実装になります。

case APP_STATE_TMR_I2C_INIT:
    //Initialize I2C and Send 'Who am I' command
    if(MPU9250_Init(&appData.mpu9250, DRV_I2C_INDEX_0, MPU9250_I2C_ADDRESS_0)) {
        MPU9250_StartReadingResistor(&appData.mpu9250, MPU9250_RA_WHO_AM_I, RXBuffer);
        appData.state = APP_STATE_MPU9250_CHECK_WHO_AM_I;
    } else
        appData.state = APP_STATE_ERROR;

    break;            
case APP_STATE_MPU9250_CHECK_WHO_AM_I:            
    switch(MPU9250_GetTransferStatus(&appData.mpu9250)) {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            if(RXBuffer[0] == MPU9250_RESPONSE_WHO_AM_I)
                appData.state = APP_STATE_READ_RAW_DATA;
            else
                appData.state = APP_STATE_ERROR;
            break;                    
        case DRV_I2C_BUFFER_EVENT_ERROR:
            appData.state = APP_STATE_ERROR;
            break;
        default:
            break;
    }            
    break;            
case APP_STATE_READ_RAW_DATA:
    if(MPU9250_StartBurstReadingResistor(&appData.mpu9250, MPU9250_RA_ACCEL_XOUT_H, RXBuffer, 14)) {
        i2cWaitCounter = 0;
        appData.state = APP_STATE_CHECK_RAW_DATA;
    } else {
        appData.state = APP_STATE_ERROR;
    }
    break;            
case APP_STATE_CHECK_RAW_DATA:
{
    DRV_I2C_BUFFER_EVENT event = MPU9250_GetTransferStatus(&appData.mpu9250);
    switch(event) {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            MPU9250_ParseVector(&RXBuffer[0], &appData.accel);
            appData.temperature = MPU9250_ParseInt16(&RXBuffer[6]);
            MPU9250_ParseVector(&RXBuffer[8], &appData.gyro);
            
            appData.state = APP_STATE_READ_RAW_DATA;

            break;                    
        case DRV_I2C_BUFFER_EVENT_ERROR:
            appData.state = APP_STATE_ERROR;
            break;
        default:    //I2C communication in operating
            if(i2cWaitCounter++ > 1000) {
                appData.state = APP_STATE_ERROR;
            }
            break;
    }
    break;
}            

まずはWhoAmIコマンドを送り、MPU-9250がちゃんと機能しているかを確認します。
その次は、加速度センサのx軸の上位8bitから14bytes連続で読みだします。ここに加速度センサの各軸の値、温度、ジャイロセンサの各軸の値が含まれます。
I2Cの読み込みが終了したら、値を分離して保存してあげます。
デフォルトで加速度は±2gFSですので、測定値の生の値を2^(16-2)で割ってから重力加速度の9.8を掛けてあげればm/s^2になります。


測定データの加速度の値です。ロギング中に基板をグリグリと回してあげたのでこのような測定結果が得られています。静置時にz軸方向におよそ9.8m/s^2が出ているので、値としてもおかしくないと思います。



…とまあここまですんなりと書きましたが、ハマりポイントはありました。

実際はこのI2C通信以外にもUSBメモリーの書き込み処理や、時間を計測するためのタイマー処理などをやっているわけで、コードは結構複雑になっています。
そんな中、データを取っていると0~1秒程度でハングアップしてしまう現象にかなり悩まされました。
I2Cがハングアップしているのか、USBがハングアップしているのか、LEDデバッグという超縛りプレイ環境のせいで切り分けに苦労しましたが 、結局はI2Cがハングアップしていました。

不具合の内容としては、上記プログラムの中でMPU9250_GetTransferStatus関数を呼び出し、I2Cの通信が完了するのを待つというところがありますが、いつまでたってもI2Cの通信が終わらないというものでした。
ですが、I2Cの不具合となれば、USBメモリーの読み書き機能が使えますので、実際にMPU9250_GetTransferStatus関数(DRV_I2C_TransferStatusGet関数)が返すDRV_I2C_BUFFER_EVENTの値と、DRV_I2C_BytesTransferred関数が返す送受信済みバイト数を記録してみました。


青線がDRV_I2C_BUFFER_EVENTの値で橙線が送受信済みバイト数です。
レジスタアドレス送信+14bytesリードで計15bytesの読み書きが行われています。
statusとしては正常に送信要求→リスタート→ACK送信の流れで読み書きができていますが、最後は5bytes分読み込んだところでブツンと受信が息絶えてしまっています。

そもそもI2Cはマスター側がクロックを発生させて、そのタイミングでスレーブがデータを送信しますので、スレーブがそれに反応できなかったとしてもプルアップ抵抗のために0xFFのデータが受かるだけです。そのため、受信中にハングアップするのは、明らかにスレーブではなくマスターのせいなわけです。


さてさて、こうなるとHarmonyのI2CドライバがDRV_I2C_TransmitThenReceive関数の呼び出しを受けて処理を開始し、各タイミングで発生する割り込みを処理し、送受信を完了させるまでの間にどこか問題があるということになって、一ユーザーがどうにかできるような領域じゃなくなってきてしまいます。困った困った。

ん…?各タイミングで発生する割り込み処理…?


設定を見ると、デフォルトでI2Cの割り込み優先度が1になっています。
タイマやUSBは4に設定されており、I2C割り込み中にもUSBの割り込みが発生することができます。

I2Cの割り込み処理をしている間に他の割り込みが入ってきて、受信データの処理が間に合わなくなったらどうなる…?ACKを送るタイミングを逃したらNACKだとスレーブが勘違いして送るのをやめるのでは…?

というわけで、優先度を最大の7にしてみました。
そうすると、無事、安定して動きましたとさ。



ふぅ、これで3日間くらい格闘してたぜ…。
これでやっと次のステップに移れる…。