2018年3月24日土曜日

ListView Extensions ver.1.0.1リリース

ListView Extensions ver.1.0.1をリリースしました。
NuGet Gallery | ListViewExtensions 1.0.1
変更点は以下の2点です。
  • Obsoleteに指定していた非同期アクセス非サポートのクラスを削除した
  • SyncedObservableCollection<T>が実装する非ジェネリックインターフェイス(IList, ICollectionなど)を明示的なインターフェイスの実装にした
どちらも破壊的な変更ですので、今までコンパイルが通っていたコードがアップデートによって通らなくなる可能性があります。ただ、その影響範囲は小さいはずです。
そもそもこの変更でコンパイルが通らなくなるようなコードは悪いコードなので悔い改めてください。


ところで、「明示的なインターフェイスの実装(explicit interface implementation)」 というC#の機能、しっかり知りませんでした…。恥ずかしい///
明示的なインターフェイスの実装 (C# プログラミング ガイド)
このような機能が必要になる状況として、上記のサイトでは、同じシグネチャのメソッドを持っている別々のインターフェイスを両方とも実装するクラスで、それぞれ別々の実装を行いたいときというのが説明されています。
それはそれで納得はいくのですが、どちらかと言えば今回重要になってくるのはこの機能の副作用?です。

明示的なインターフェイスの実装では、インスタンスをそれぞれのインターフェイスにキャストしない限りそれぞれのメソッドにアクセスできないという制約が発生します。当然ですね、別々の実装があるのですから、呼び出し時にはそれを絞り込むための制約が必要です。
これを活用することで、別にシグネチャが重複していなくても、IListやICollectionなどの非ジェネリックメソッドを積極的には呼び出して欲しくないときに、非ジェネリックインターフェイスを明示的に実装することで、呼び出し側も非ジェネリックインターフェイスにキャストしないと呼び出せなくすることができます。
例えば、List<T>が非ジェネリックのIListも実装しているにもかかわらず、Visual StudioのIntelliSenseに非ジェネリックメソッドが出てこないのはこういうことだったのですね。

ちなみに、明示的なインターフェイスの実装ではアクセス修飾子(publicなど)は付けられません。明示的なインターフェイスの実装である時点でインターフェイスの実装だというのがわかっているので自動的にpublicになるようです。
通常、クラスではアクセス修飾子を省略するとprivateになるのでちょっと違和感はありますね。

話がだいぶそれてしまいましたが、この点を修正したListView Extensions 1.0.1をよろしくお願いします。

2018年2月25日日曜日

MVVMにおけるViewModelとModelのプロパティ同期 - NotifyPropertyHelper 1.0.0-beta1

MVVMスタイルでWPFアプリケーションなどを作っていると、ViewModelとModelにやたら同じプロパティが出てきます。

それもそのはず、ViewModelはViewの実装上の制約を吸収する層なので、画面の状態(表示内容など)自体はModelもプロパティとして持っています。そうでなければMVVMではありません。詳しくは以前書いた記事を読んでください。
MVVMとは何か
ですが、その通りに実装すると同じ名前のプロパティがViewModelとModelで大量に出てきてしまうという問題もありました。
MVVMのサンプルプログラム - TwitterViewer  
こちらのページで紹介しているサンプルプログラムは基本的にはTwitterからツイートを持ってきて画面に表示するだけですので、ModelのプロパティもGet-Onlyのプロパティばかりです。ですが、もっと動的に内容が動くような状況の場合は、もっとしっかりViewModelにModelの変更通知を受けてプロパティをコピーするコードや、逆にViewの操作で変化したプロパティをModelにコピーするコードを書かなければなりません。

問題提起

サンプルプログラム

例えば、指定したURLのHTMLを取得するアプリケーションを作るとします。

このアプリケーションはURLを入力しGoボタンを押すとそのサイトのHTMLを取得して画面に表示するシンプルなものです。これをMVVMで作ると次のようなコードになります。

【View】
<Window x:Class="Frontend.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:Frontend.Views"
        xmlns:vm="clr-namespace:Frontend.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <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>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="1*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="1*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Row="0" Grid.Column="0" Text="URL: " VerticalAlignment="Center" />
            <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Url, UpdateSourceTrigger=PropertyChanged}" />
            <Button Grid.Row="0" Grid.Column="2" Content="Go" Width="50" Command="{Binding GoCommand}" />
            <TextBox Grid.Row="1" Grid.ColumnSpan="3" IsReadOnly="True" Text="{Binding Html}"
                     ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.VerticalScrollBarVisibility="Visible" />
        </Grid>
    </Grid>
</Window>

【ViewModel】
public class MainWindowViewModel : ViewModel
{
    Model model;

    public void Initialize()
    {
        model = Model.GetInstance();

        GoCommand = new ViewModelCommand(() => model.Go(), () => !string.IsNullOrEmpty(Url));
        GoCommand.RaiseCanExecuteChanged();

        this.PropertyChanged += This_PropertyChanged;
        model.PropertyChanged += Model_PropertyChanged;

        this.Url = model.Url;
        this.Html = model.Html;
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Url):
                model.Url = this.Url;
                GoCommand.RaiseCanExecuteChanged();
                break;
        }
    }

    private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Model.Html):
                this.Html = model.Html;
                break;
        }
    }

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

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

    #endregion

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

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

    #endregion

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

    public ViewModelCommand GoCommand
    {
        get { return _GoCommand; }
        set
        {
            if(_GoCommand == value)
                return;
            _GoCommand = value;
            RaisePropertyChanged(nameof(GoCommand));
        }
    }
    private ViewModelCommand _GoCommand;

    #endregion

    protected override void Dispose(bool disposing)
    {
        if(disposing) {
            this.PropertyChanged -= This_PropertyChanged;
            model.PropertyChanged -= Model_PropertyChanged;
        }

        base.Dispose(disposing);
    }
}

【Model】
public class Model : NotificationObject
{
    #region Singleton

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

    #endregion

    private Model()
    {
        Url = @"https://www.google.co.jp/";
    }

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

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

    #endregion

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

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

    #endregion

    public async void Go()
    {
        using(WebClient wc = new WebClient()) {
            try {
                Html = await wc.DownloadStringTaskAsync(Url);
            }
            catch(Exception e) {
                Html = e.Message;
            }
        }
    }
}
例によってLivetを使っています。

大まかな流れとしては

ViewでURLを入力→ViewModel→Modelへ入力値が伝搬する
↓
ViewでGoボタンを押す→ViewModel→Modelへメソッドの呼び出しが伝搬する
↓
ModelがGoメソッド内でUrlプロパティの値を使ってHTMLをダウンロードし、Htmlプロパティにセットする
↓
ModelがHtmlプロパティが変化したことをイベントで通知する
↓
Model→ViewModel→ViewへHtmlプロパティが伝搬する。この時、ViewModelはViewの制約である「単一スレッドでしか動作をしない」を受け、UIスレッドでHtmlプロパティの値を更新する。
↓
ViewにHTMLが表示される

となります。まさに厳密なMVVMです。ViewModelはViewの実装上の制約を吸収するためのみの仕事に徹し、ModelはViewの実装上の制約などは何も気にせずのびのびと.NETの機能を使っています。

さて、ですが実際にこのコードを書いていると、問題点も見えてきます。

ViewModelとModelのプロパティ同期関係のコードが散在する

当然、ModelとViewModelを同期するのはViewModelの仕事となるわけですが、この同期作業は割と面倒です。
やることとしては、
  1. 初期化時に値を同期する作業
  2. Modelのプロパティ変化時にViewModelのプロパティを更新する作業
  3. ViewModelのプロパティ変化時にModelのプロパティを更新する作業
  4. View消滅時にModelの監視をやめる作業
の3つになります。
実際に書いたコードがこんな感じになります。

この画像を見れば一目瞭然です。あちこちにプロパティ同期のコードが散らばっています。
ViewModelの仕事がプロパティ同期だけならまだいいのですが、他にもCommandの管理など、ModelとViewを繋ぐ仕事は沢山あります。そうすると、それぞれの仕事があちこちに散在してとても見にくいプログラムになりますし、あとからプロパティを増やすときなんかは変更忘れの原因にもなってしまいます。

正直言ってこんなのやってられません。何かもっと簡単に同期作業ができるライブラリが必要です。
ですが、たいていのMVVMライブラリの関心ごとは「ViewとViewModelをいかにして連携させるか」です。ViewModelとModelは通常のC#の言語機能を用いて連携できますから、勝手にやってくださいどうぞというスタンスになってしまうのもある程度しかたのないことなのでしょうが…。

NotifyPropertyHelper

さて、というわけで、ModelとViewModelの連携に的を絞ったライブラリを書いてみました。

1. プロパティの同期

流れとしては、
  1. 同期元の同期したいプロパティにPropertySync属性を付ける
  2. PropertySyncServiceで同期元と同期先のクラスを指定する
  3. 同期元がDisposeされたときにPropertySyncServiceをDisposeする
だけになります。あまり変わらないじゃん!って思うかもしれませんが、プロパティを追加したときに、プロパティの追加と、その追加したプロパティに属性を追加するだけですものは結構でかいです。

上記のViewModelを書き換えてみます。

public class MainWindowViewModel : ViewModel
{
    Model model;
    PropertySyncService propsync;

    public void Initialize()
    {
        model = Model.GetInstance();

        GoCommand = new ViewModelCommand(() => model.Go(), () => !string.IsNullOrEmpty(Url));
        GoCommand.RaiseCanExecuteChanged();

        this.PropertyChanged += This_PropertyChanged;

        propsync = new PropertySyncService(this, model);
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Url):
                GoCommand.RaiseCanExecuteChanged();
                break;
        }
    }

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

    [PropertySync(PropertySyncMode.TwoWay, Direction.TargetToSource)]
    public string Url
    {
        get { return _Url; }
        set
        {
            if(_Url == value)
                return;
            _Url = value;
            RaisePropertyChanged(nameof(Url));
        }
    }
    private string _Url;

    #endregion

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

    [PropertySync(PropertySyncMode.OneWayToSource, Direction.TargetToSource)]
    public string Html
    {
        get { return _Html; }
        set
        {
            if(_Html == value)
                return;
            _Html = value;
            RaisePropertyChanged(nameof(Html));
        }
    }
    private string _Html;

    #endregion

    #region GoCommand変更通知プロパティ
    private ViewModelCommand _GoCommand;

    public ViewModelCommand GoCommand
    {
        get
        { return _GoCommand; }
        set
        { 
            if(_GoCommand == value)
                return;
            _GoCommand = value;
            RaisePropertyChanged(nameof(GoCommand));
        }
    }
    #endregion

    protected override void Dispose(bool disposing)
    {
        if(disposing) {
            propsync.Dispose();
        }

        base.Dispose(disposing);
    }
}
同期したいプロパティにはPropertySyncという属性を付加します。
public PropertySyncAttribute(PropertySyncMode Mode, Direction InitializeCopyDirection, Type PropertyConverter = null);
public PropertySyncAttribute(string TargetPropertyName, PropertySyncMode Mode, Direction InitializeCopyDirection, Type PropertyConverter = null);
PropertySync属性はこのような2つのコンストラクタを持っています。前者はTargetPropertyNameが省略されていますが、省略された場合は同名のプロパティを当たります。
PropertySyncModeは同期方向で、InitializeCopyDirectionが初期化時(PropertySyncServiceインスタンス生成時)にコピーをする方向です。注意点としては、同期元、同期先ともに、これらの同期方向に支障が無いようなアクセサビリティ・型のプロパティにする必要があるということです。
PropertyConverterはプロパティを変換するクラスです。後述します。

後は、クラスの生成時(コンストラクタ等)でPropertySyncServiceのインスタンスを生成し、そこで同期元と同期先のインスタンスを設定してあげます。不要になったらDisposeを呼び出すことで、同期処理を打ち切ります。

2. 型の違うプロパティの同期

ときどき型の違うプロパティを同期したくなります。
例えば、ModelではReadOnlyObservableCollectionだけど、ViewModelはUIスレッドに同期したReadOnlyObservableCollectionにしたいときなどです。
そのようなときは、IPropertyConverter<TSource, TTarget>を継承した変換クラスを作り、PropertySync属性のコンストラクタに指定してあげるとそれが実現できます。

例えば、ListViewExtensionsではModel用にSortableObservableCollection、ViewModel用にListViewViewModelを用意していますが、それらを変換するには次のようなコードを書けばよいでしょう。
class PersonListViewConverter : IPropertyConverter<ListViewViewModel<PersonViewModel, Person>, SortableObservableCollection<Person>>
{
    public ListViewViewModel<PersonViewModel, Person> ConvertToSource(ListViewViewModel<PersonViewModel, Person> OldSourceValue, SortableObservableCollection<Person> NewTargetValue)
    {
        if(OldSourceValue != null)
            OldSourceValue.Dispose();

        if(NewTargetValue == null)
            return null;
        else
            return new ListViewViewModel<PersonViewModel, Person>(NewTargetValue, person => new PersonViewModel(person), DispatcherHelper.UIDispatcher);
    }

    public SortableObservableCollection<Person> ConvertToTarget(SortableObservableCollection<Person> OldTargeteValue, ListViewViewModel<PersonViewModel, Person> NewSourceValue)
    {
        throw new InvalidOperationException("Not defined.");
    }
}
2つのConvertToSourceとConvertToTargetの2つのメソッドは古い値も引き渡してくれるため、Disposeが必要なインスタンスに対しても適切な措置を取ることができます。

3.  ReadOnlySynchronizationContextCollection

さて、今回のブログ記事のテーマとはちょっと違うのですが、ViewModelとModelの連携を取るためのライブラリということで、このようなコレクションも用意しています。

WPFではUIが単一スレッドからしかアクセスできません。そのような制約の吸収はViewModelがやることです。外から変更させる気が無いリストの場合は、ModelがReadOnlyCollectionでリストを公開し、それをViewModelがUIのスレッドに合わせながらUIに橋渡ししてあげる必要があります。それをやってくれるのがReadOnlySyncronizationContextCollectionです。

使い方は使ってみればわかると思います(雑)。

ライセンス

以下の各項目をお守りください
  • このライブラリを利用する方は自己責任でお願いします。いかなる問題が起きても作者は責任を負いません。
  • このソフトを悪用しないでください。
  • このソフトウェアを無断で単体での転載、再配布しないでください。ただし、このライブラリを参照しているソフトウェアと一緒に配布する場合を除きます。
  • 作者は使用方法やバグに関するサポートをする義務を負いません。
  • 有償アプリケーションには使用してはならない。
  • 完成したソフトウェアのどこか(ヘルプ、バージョン情報など)と、ReadMeなどのドキュメンテーションに私のライブラリを使用したことを明記すること。ただし、作者(私)がこのライブラリを自分のソフトで使用するときはその限りではない。

公開

Nugetにて公開しています。
NotifyPropertyHelper - Nuget
なお、 プレリリース版ですので、VisualStudioからの検索時にはプレリリースもヒットするオプションを指定するようにしてください。

2018年2月20日火曜日

WPFにおけるINotifyDataErrorInfoを使った値の検証 属性版

2年ちょっと前にINotifyDataErrorInfoを使った値の検証の方法を記事にしました。

これはViewModelを継承したクラスにエラー情報を蓄積する機能を持たせ、任意のタイミングでエラーを登録したり削除したりすることができるというものでした。
実際、それでも使い物にはなるのですが、しっかりとMVVMを実装しようとすると妙に不便に思えてきました。

簡単な値の検証(例えばnullじゃなければ受け付ける、など)ならまだしも、複雑な判定ロジックになるとModel側にその判定機能を持たせたくなります。なぜならば、入力値を受けて何かしらの処理を行うのはModelの仕事だからです。何かしらの処理を行う前には、渡されたパラメーターが正しいものかどうかを判定し、間違っていれば何かしらのエラー通知(例外を吐く等々)をするかと思いますが、値の検証をViewModelでやっていたとすると、ViewModelとModelの両方に複雑な判定ロジックを持たせるメリットがありません。
入力された値の正当性も状態の1つだと考えると、Modelがその状態を持っていても変ではありません。すなわち、ViewModelは入力された値をそのままModelに横流ししておき、Modelがその横流しされた値の正当性を検証して状態として公開し、それをViewModelが監視してUIに何かしらの表示を行うといった流れになれば、正当性の判断をすべてModelがすることができますし、ViewModelもそれを監視して入力欄のエラー表示やボタンの有効/無効などを切り替えればいいわけです。

そうすると、前回の記事で書いたクラスでは多少の不満が出てきます。ViewModelがModelの正当性を表すプロパティを監視し、そのプロパティの変化に合わせてエラーをセットするロジックをViewModelに書くことになります。それ自体はMVVMの考え方には反しないのですが、「あるプロパティの値の正当性を他のとあるプロパティが示している」というシンプルな関係なのに、なぜわざわざプロパティの変更通知を購読し、値が変化したタイミングでメソッドを呼び出して…なんて面倒くさい処理を書かなきゃいけないのかと。実際書いてみると、誰もが「もっとシンプルにプロパティの関係性を書きたい!」と思うはずです。

そこで、そのような機能を実現するViewModelを作ってみました。
public abstract class NotifyDataErrorViewModel : ViewModel, INotifyDataErrorInfo
{
    Dictionary<string, NotifyErrorAttribute[]> properties;

    public NotifyDataErrorViewModel()
    {
        properties = this
            .GetType()
            .GetProperties()
            .Select(p => new KeyValuePair<string, NotifyErrorAttribute[]>(
                p.Name,
                p.GetCustomAttributes(typeof(NotifyErrorAttribute), true)
                    .Cast<NotifyErrorAttribute>()
                    .Where(p1 => p1.IsValidProperty(this))
                    .ToArray()))
            .Where(p => p.Value.Length > 0)
            .ToDictionary(p => p.Key, p => p.Value);

        this.PropertyChanged += This_PropertyChanged;

        foreach(var p in properties.Where(p => p.Value.Any(p1 => !p1.GetValidityValue(this))).Select(p => p.Key))
            RaiseErrorsChanged(p);
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        foreach(var p in properties.Where(p => p.Value.Any(p1 => p1.ValidityPropertyName == e.PropertyName)))
            RaiseErrorsChanged(p.Key);
    }

    /// <summary>
    /// エラーがあるかどうかを示すプロパティ
    /// </summary>
    public bool HasErrors => properties.Values.SelectMany(p => p).Any(p => !p.GetValidityValue(this));

    /// <summary>
    /// エラー情報を返すメソッド
    /// </summary>
    /// <param name="propertyName">エラーがあるか調べたいプロパティ名</param>
    /// <returns>エラーがあればエラーテキストの配列、無ければ空の配列</returns>
    public IEnumerable GetErrors(string propertyName)
    {
        if(properties.ContainsKey(propertyName))
            return properties[propertyName].Where(p => !p.GetValidityValue(this)).Select(p => p.ErrorText).ToArray();
        else
            return Enumerable.Empty<string>();
    }

    /// <summary>
    /// エラー状態が変わった時に発生するイベント
    /// </summary>
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    /// <summary>
    /// ErrorsChangedイベントを発生させるメソッド
    /// </summary>
    /// <param name="PropertyName">プロパティ名</param>
    protected void RaiseErrorsChanged(string PropertyName)
    {
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(PropertyName));
    }
}
ViewModelを継承した抽象クラスで、INotifyDataErrorInfoを実装しています。
ここまでは前回と同じなのですが、エラーを登録/削除するメソッドは用意していません。さて、ではどうやってエラーの登録/削除をするのでしょう。

その答えは属性です。
public class NotifyErrorAttribute : Attribute
{
    public NotifyErrorAttribute(string ValidityPropertyName, string ErrorText)
    {
        this.ValidityPropertyName = string.IsNullOrEmpty(ValidityPropertyName) ? throw new ArgumentNullException(nameof(ValidityPropertyName)) : ValidityPropertyName;
        this.ErrorText = string.IsNullOrEmpty(ErrorText) ? throw new ArgumentNullException(nameof(ErrorText)) : ErrorText;
    }

    public string ValidityPropertyName { get; }

    public string ErrorText { get; }

    /// <summary>
    /// この属性で与えられたプロパティ名が、指定したオブジェクトで値の正当性を示しているプロパティとして正当かを検証します。
    /// </summary>
    /// <param name="Source">オブジェクト</param>
    /// <returns>指定したオブジェクトがbool型の読み取り可能なこの名前のプロパティを持っていた場合true、そうでなければfalse</returns>
    internal bool IsValidProperty(object Source)
    {
        var property = Source.GetType().GetProperty(ValidityPropertyName);
        return property != null && property.CanRead && property.PropertyType == typeof(bool);
    }

    /// <summary>
    /// 値の正当性を取得します。
    /// </summary>
    /// <param name="Source">オブジェクト</param>
    /// <returns>値の正当性</returns>
    internal bool GetValidityValue(object Source)
    {
        return (bool)Source.GetType().GetProperty(ValidityPropertyName).GetValue(Source);
    }
値の正当性情報を付加したいViewModelのプロパティにこの属性を付け、値の正当性を示しているプロパティの名前とエラーが起きたときのメッセージを渡してあげます。そうすることで、NotifyDataErrorViewModelクラスは値の正当性を示しているプロパティを監視して、エラー状態が変化すると自動的にErrorsChangedイベントを発生させます。
NotifyErrorAttributeを1つのプロパティに複数付けた場合、1つでもエラーが起こるとエラーとみなします。GetErrorsメソッドが返すエラーメッセージもそのエラーの内容によって変わるので、エラーになる条件が複数ある場合なんかは属性をいくつか付けるといいでしょう。


さて、せっかくですので以前の記事の時に作ったサンプルプログラムと同じような動きをするサンプルプログラムを今回のクラスを使って作ってみました。
Modelは次の通りです。
public class Model : NotificationObject
{
    #region Singleton

    static Model Instance;

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

    #endregion

    private Model()
    {
        this.PropertyChanged += This_PropertyChanged;

        CheckValidity();
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Number):
            case nameof(Digits):
                CheckValidity();
                break;
        }
    }

    void CheckValidity()
    {
        NumberIsANumber = int.TryParse(Number, out int number);
        DigitsIsANumber = int.TryParse(Digits, out int digits);

        if(NumberIsANumber && DigitsIsANumber) {
            if(number == 0)
                NumberHasTheDigits = digits == 1;
            else
                NumberHasTheDigits = (int)Math.Log10(Math.Abs(number)) + 1 == digits;
        } else
            NumberHasTheDigits = true;
    }

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

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

    #endregion

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

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

    #endregion

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

    public bool NumberHasTheDigits
    {
        get { return _NumberHasTheDigits; }
        set
        {
            if(_NumberHasTheDigits == value)
                return;
            _NumberHasTheDigits = value;
            RaisePropertyChanged(nameof(NumberHasTheDigits));
        }
    }
    private bool _NumberHasTheDigits;

    #endregion

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

    public bool DigitsIsANumber
    {
        get { return _DigitsIsANumber; }
        set
        {
            if(_DigitsIsANumber == value)
                return;
            _DigitsIsANumber = value;
            RaisePropertyChanged(nameof(DigitsIsANumber));
        }
    }
    private bool _DigitsIsANumber;

    #endregion

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

    public bool NumberIsANumber
    {
        get { return _NumberIsANumber; }
        set
        {
            if(_NumberIsANumber == value)
                return;
            _NumberIsANumber = value;
            RaisePropertyChanged(nameof(NumberIsANumber));
        }
    }
    private bool _NumberIsANumber;

    #endregion
}
NumberとDigitsというプロパティを持っており、これらの値が変更されるたびにCheckValidityメソッドを呼び出しています。ここからNumberHasTheDigitsプロパティ、DigitsIsANumberプロパティ、NumberIsANumberプロパティの3つを変更しています。NumberIsANumberはなんか哲学的な名前になってしまっていますが、xxxxIsANumberのプロパティがxxxxのプロパティが値であるかどうか、NumberHasTheDigitsはNumberがDigitsの桁数を持っているかを判定するフラグです。ただ、NumberとDigitsのどちらかのみが値だった場合にfalseにしてしまうと芸がない(というか、xxxxIsANumberプロパティの存在価値がなくなる)ので、片側が入力されている状態ではtrueにしてみました。

肝心のViewModelはこちらです。
public class MainWindowViewModel : NotifyDataErrorViewModel
{
    Model model;

    public void Initialize()
    {
        model = Model.GetInstance();

        this.PropertyChanged += This_PropertyChanged;
        model.PropertyChanged += Model_PropertyChanged;
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Number):
                model.Number = Number;
                break;
            case nameof(Digits):
                model.Digits = Digits;
                break;
            case nameof(NumberHasTheDigits):
                model.NumberHasTheDigits = NumberHasTheDigits;
                break;
            case nameof(DigitsIsANumber):
                model.DigitsIsANumber = DigitsIsANumber;
                break;
            case nameof(NumberIsANumber):
                model.NumberIsANumber = NumberIsANumber;
                break;
        }
    }

    private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Model.Number):
                Number = model.Number;
                break;
            case nameof(Model.Digits):
                Digits = model.Digits;
                break;
            case nameof(Model.NumberHasTheDigits):
                NumberHasTheDigits = model.NumberHasTheDigits;
                break;
            case nameof(Model.DigitsIsANumber):
                DigitsIsANumber = model.DigitsIsANumber;
                break;
            case nameof(Model.NumberIsANumber):
                NumberIsANumber = model.NumberIsANumber;
                break;
        }
    }

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

    [NotifyError(nameof(NumberHasTheDigits), "The Number does not match to the Digits.")]
    [NotifyError(nameof(NumberIsANumber), "The Number is not a number.")]
    public string Number
    {
        get { return _Number; }
        set
        {
            if(_Number == value)
                return;
            _Number = value;
            RaisePropertyChanged(nameof(Number));
        }
    }
    private string _Number;

    #endregion

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

    [NotifyError(nameof(NumberHasTheDigits), "The Digits does not match to the Number.")]
    [NotifyError(nameof(DigitsIsANumber), "The Digits is not a number.")]
    public string Digits
    {
        get { return _Digits; }
        set
        {
            if(_Digits == value)
                return;
            _Digits = value;
            RaisePropertyChanged(nameof(Digits));
        }
    }
    private string _Digits;

    #endregion

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

    public bool NumberHasTheDigits
    {
        get { return _NumberHasTheDigits; }
        set
        {
            if(_NumberHasTheDigits == value)
                return;
            _NumberHasTheDigits = value;
            RaisePropertyChanged(nameof(NumberHasTheDigits));
        }
    }
    private bool _NumberHasTheDigits;

    #endregion

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

    public bool DigitsIsANumber
    {
        get { return _DigitsIsANumber; }
        set
        {
            if(_DigitsIsANumber == value)
                return;
            _DigitsIsANumber = value;
            RaisePropertyChanged(nameof(DigitsIsANumber));
        }
    }
    private bool _DigitsIsANumber;

    #endregion

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

    public bool NumberIsANumber
    {
        get { return _NumberIsANumber; }
        set
        {
            if(_NumberIsANumber == value)
                return;
            _NumberIsANumber = value;
            RaisePropertyChanged(nameof(NumberIsANumber));
        }
    }
    private bool _NumberIsANumber;

    #endregion
}
前半にあるのは自身とModel両方の値の変化をウォッチして同期するプログラムです。
NumberとDigitsにはNotifyError属性を付けて、どのプロパティが自身の正当性を示しているかを記述しています。これだけでコントロールに正当性表示が行われるなんて夢の酔うでしょ?

ん?DataAnnotations?
…知らね…そんなの…。
(マジレスするとDataAnnotationsはCustomValidationAttributeが任意のメソッドを判定に指定できてしまうため、INotifyPropertyChangedの監視とリフレクションだけじゃ対応しきれずここまでスマートな実装はできないのです…。また、Modelでの値の検証を反映するという考えからも外れてしまいます…。)

2018年1月24日水曜日

MPLAB Xでアセンブリリストを見る

私が初めてPICを触ったころ、それは10年以上前になりますが、その時はまだCコンパイラは数百ドルする有料版しかありませんでした。当時中学生だった私は、当然そのような高価なものは買えるわけもなく、一生懸命PICのアセンブリを覚えてプログラムを書いていました。

時代は変わり、PIC以外にもたくさんのマイコンが趣味レベルで使われるようになり、開発環境もC言語が主流になりました。PICも一足遅れて(実用レベルの)Cコンパイラを無償公開し、今ではMicrochipが買収したHi-Tech社のCコンパイラがXCコンパイラとしてPICの基幹コンパイラになっています。

今ではC言語向けに間接参照などの機能を強化したPICがたくさん出てきており、アセンブリで開発する理由は皆無ですが、それでもときどき「このコードはどのような形でアセンブリに展開されるのだろうか」と気になることがあります。

例えばEEPROMアクセス。
EEPROMは、不意な書き込みを防止するため、アンロックシーケンスと呼ばれる一定の手順で命令を実行しないと書き込みできないようになっています。
        BCF     INTCON, GIE
        MOVLW   55h
        MOVWF   EECON2
        MOVLW   AAh
        MOVWF   EECON2
        BSF     EECON1, WR
        BSF     INTCON, GIE
これが代表的なPIC16シリーズにおけるEEPROM書き込みシーケンスの一部です。EECON2には0x55を書き込んだ2サイクル後に0xAAを書き込み、さらに次のサイクルでEECON1のWRビットをセットしなければいけないため、C言語で書いていてもこの部分だけはインラインアセンブリで書かなければならなくなることがあります。
ですが、C言語で書いても次のように書けば上記のアセンブリに展開されることは容易に想像が付きます。
INTCONbits.GIE = 0;
EECON2 = 0x55;
EECON2 = 0xAA;
EECON1bits.WR = 1;
INTCONbits.GIE = 1;
でもまあ、C言語をどうアセンブリに展開するかはコンパイラの勝手なので、こういう書き方をすれば絶対に展開されるとは限りません。こういう時にどのような形でアセンブリ展開されるか見てみたくなりますよね。

アセンブリリストは…っとその前に、まずはお約束の環境を書いておきます。
  • MPLAB X IDE v4.05 
  • XC8 v1.45

アセンブリリストはメニューの
Window → Debugging → Output → Disassembly Listing File
で開くことができます。
ですがおそらく、最初は次のようなファイルが開かれて、アセンブリリストは出てこないはずです。

悩むほど難しい英語ではないですが、一応拙訳を付けておきます。
アセンブリリストの生成は無効になっています。
下記の情報に従ってプロジェクトをビルドしてください。
  1. プロジェクトのプロパティを開いてください。
  2. 「Conf: [現在の設定]」ノードの下にある「Loading」を選択してください。
  3. 「Load Symbols when Programming or building for production」にチェックを入れてください。
このエディタ上でダブルクリックをすると上記の設定ができます。
最後の1行にしれっと書いてありますが、エディタ上をダブルクリックすればワンタッチでダイアログの当該部分が表示される超便利機能があります。
上のほうにある「Load Symbols when Programming or building for production」にチェックを入れて、OKを押してプロジェクトをリビルドしましょう。
ご覧の通り、ばっちりアセンブリリストが表示されました。

実はXC8には組み込み関数でeeprom_writeとeeprom_readというのがあるので、直接レジスタを叩かなくてもEEPROMの読み書きはできるのですが、その部分のコードも展開されていることが分かります。
PICの違いによるレジスタ名の違いで、EECONxではなくNVMCONxになっていますが、5607行目からアンロックシーケンスが始まっていることが分かります。
書き込みを開始してから完了までの間は割り込みを禁止したままなんですね。CPUは動作しているのでそのようにする理由もあまり無い気もしますが…(割り込み中にEEPROMを読み出すのなら話は別ですが)。

2018年1月17日水曜日

LivetにおけるWPFでのダイアログ表示のいろは

いっつもLivetを使っているはずなのに、いろいろなクラスの名前をすっかり忘れてしまってその都度ググることになってしまっているので、備忘録的にまとめます。

基本

TriggerAction

画面遷移は基本的にTriggerActionを使います。その名の通り、何かしらの引き金によって発動するアクションです。
TriggerActionを発動させる方法はいくつかありますが、イベントで発動させる方法とViewModelから発動させる方法を覚えておけば困らないでしょう。前者はEventTrigger(System.Windows.Interactivityの機能)、後者はMessenger(Livetの機能)を使います。

EventTrigger

こちらは、例えばダイアログ表示ではなくても、Livetのメソッド直接バインディング機能などを使うときに多用することになるでしょう。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="ButtonPushed" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
例えばこんなやつです。ボタンを押すとEventTriggerでClickイベントを拾い、LivetCallMethodActionを発動させるコードです。基本的にレイアウトなどの動きが無いコードになりがちなマークアップ言語に一気に彩りが生まれる記法ですので是非押さえておいてください。

Messenger

LivetにはMessengerという機能があります。ViewModelはMessengerというインスタンスを持っており、MessengerでInteractionMessageクラスのメッセージを送信すると、それをViewで受け取れる(=TriggerActionとして動作する)というものです。
標準のWPFにはこのようにViewModelからViewに何かを働きかける機能がかなり少なく、Livet以外のMVVMライブラリでもたいてい似たような機能は実装されているようです。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MethodCaller">
        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="MessageRaised" />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
これは上記のEventTriggerの例になぞらえて、代わりにMessengerでLivetCallMethodActionを発動させるプログラムです。EventTriggerがInteractionMessageTriggerに代わっただけですので難なく理解できるかと思います。
ちなみに、ViewModelではこのように呼び出します。
Messenger.Raise(new InteractionMessage("MethodCaller"));
InteractionMessageクラスでキーを指定します。これによって、単一のコントロール内に複数のInteractionMessageTriggerを持っていても使い分けることができます。

InteractionMessage

上記のMessengerの項でも少し出てきましたが、MessengerとはViewModelからViewに情報を受け渡すものです。上記の例ではRaiseしかしていないのでMessengerKeyしか渡しませんでしたが、例えばダイアログボックスならば表示テキストやキャプション、アイコンの種類などを伝える必要があるでしょう。そのような場合は、InteractionMessageを継承した派生クラスを作ってパラメーターを持たせるような仕様になっています。それに対応して、TriggerActionの派生クラスであるInteractionMessageActionを継承した派生クラスも作成し、そこで独自のInteractionMessageを受け取れば良いです。

Messengerの例

というわけで、まずはMessengerを使ってダイアログボックスを表示する例を示します。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MessageBox">
        <l:InformationDialogInteractionMessageAction />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
Messenger.Raise(new InformationMessage("Message Text", "Caption", MessageBoxImage.Information, "MessageBox"));
ViewとViewModelはこんな感じになります。
InformataionDialogInteractionMessageActionは単一ボタン(=OKボタンのみ)のメッセージボックスを表示するTriggerActionです。
これに対応するInteractionMessageはInformationMessageで、これにメッセージ内容、キャプション内容、アイコン、メッセージキーを含めてMessengerに乗せることでダイアログボックスを表示させることができます。

EventTriggerの例

ところで、ボタンが押されたらそのままメッセージボックスを表示したくなることがあります。ですが、上記のMessengerの例を見ているとViewModelからViewにメッセージを送る必要があるので、ボタン(View)—(メソッド呼び出し)→Messenger発動(ViewModel)—(Messenger)→ダイアログ表示(View)といった具合にViewとViewModelを往復する必要が出てきてしまいます。これではあまりスマートではありません。

ですがそこは流石Livet、ちゃんと方法が用意されています。
InteractionMessageActionクラスにはDirectInteractionMessageプロパティが用意されており、ここに直接InteractionMessageを渡してあげればいい…と思いきや、ここに渡すのはDirectInteractionMessageクラスです。これについては細かくは後述します。
DirectInteractionMessageにはMessageプロパティがあり、こちらにInteractionMessageを渡せばいいです。
さらに、それぞれのクラスのDirectInteractionMessageプロパティ、MessageプロパティはContentPropertyに設定されているので、プロパティを明示しなくても設定できるのでネストが深くなるのも最小限に抑えることができます。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click" >
            <l:InformationDialogInteractionMessageAction>
                <l:DirectInteractionMessage>
                    <l:InformationMessage Caption="Caption" Text="Message text" Image="Information" />
                </l:DirectInteractionMessage>
            </l:InformationDialogInteractionMessageAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
こんな感じです。InformataionDialogInteractionMessageActionに直接InformationMessageを渡すことができています。これによって、View単体で(ViewModelを介在せずに)ダイアログを表示することができます。

ダイアログの戻り値

さて、上記は単一ボタンのダイアログを表示していたので戻り値は特に必要ありませんでした。ですが、例えば「はい」「いいえ」で聞くダイアログなどは戻り値を知る必要があります。

Messengerの例

実はこの戻り値はMessengerが持つようにできています。ですので、Messengerの例は特に上記の例とほぼ同じコードになります。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MessageBox">
        <l:ConfirmationDialogInteractionMessageAction />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
var msg = new ConfirmationMessage("Is this a MessageBox?", "Caption", MessageBoxImage.Question, MessageBoxButton.YesNoCancel, "MessageBox");
Messenger.Raise(msg);
if(msg.Response == null) {
    // Cancel clicked
} if(msg.Response.Value) {
    // Yes clicked
} else {
    // No clicked
}
bool?型のResponseプロパティに結果が入るので、Messaenger.Raiseから制御が返ってきた後にそれを見れば問題ありません。

EventTriggerの例

さて、ここでまた問題が発生します。当然、ダイアログの戻り値はソフトウェアの制御側(=ViewModel)で受け取りたいですが、EventTriggerで直接TriggerActionを呼び出してしまった場合、ViewModelは介在しないのでそのMessageを受け取るところがありません。

ですがそこは流石Livet、ちゃんと方法が用意されています。
先ほど無駄に挟んでいたように見えたDirectInteractionMessageクラスですが、実は答えはここにあります。これがCallbackMethodTarget/CallbackMethodNameプロパティ及びCallbackCommandプロパティを持っていて、ここでViewModelのメソッドやコマンドをバインディングすることによってViewModelが戻り値を受け取ることができるようになります。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click" >
            <l:ConfirmationDialogInteractionMessageAction>
                <l:DirectInteractionMessage CallbackMethodTarget="{Binding}" CallbackMethodName="MessageBoxClosed">
                    <l:ConfirmationMessage Caption="Is this a MessageBox?" Text="Message text" Image="Question" Button="YesNoCancel" />
                </l:DirectInteractionMessage>
            </l:ConfirmationDialogInteractionMessageAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
public void MessageBoxClosed(ConfirmationMessage msg)
{
    if(msg.Response == null) {
        // Cancel clicked
    }
    if(msg.Response.Value) {
        // Yes clicked
    } else {
        // No clicked
    }
}
ViewModelではDirectInteractionMessageに渡していたInteractionMessageをパラメーターにしたメソッドを用意しておく必要があります。これで、ボタンからメッセージボックスを表示し、その結果をViewModelで受け取ることができます。

クラス名対応表

さて、Messaengerとかはもうわかっているからいいよという人はここだけ見てください。
用途 TriggerAction InteractionMessage 備考
メッセージボックス(単一ボタン) InformationDialogInteractionMessageAction InformationMessage
メッセージボックス(複数ボタン) ConfirmationDialogInteractionMessageAction ConfirmationMessage
ファイルを開くダイアログ OpenFileDialogInteractionMessageAction OpeningFileSelectionMessage
ファイルを保存ダイアログ SaveFileDialogInteractionMessageAction SavingFileSelectionMessage
フォルダ選択ダイアログ FolderBrowserDialogInteractionMessageAction FolderSelectionMessage Livet.Extensionsが必要
任意のウィンドウ TransitionInteractionMessageAction TransitionMessage
Livetが持っているダイアログ表示関係のTriggerAction/InteractionMessageはこれで全部のはずです。ほかにも、Windowを最大化/最小化/閉じるなどを実現するWindowInteractionMessageAction/WindowActionMessageなどもありますが、まあこれは自分で調べて使ってみてください。

最後の任意のウィンドウ表示はなかなかに曲者かと思います。特に、これで開くウィンドウに対応するViewModelをどうしたらいいのか、というところでとても悩むことになるかと思います。
まあその辺はまたの機会に。

2018年1月13日土曜日

eventをasync/awaitに変換する

今回のテーマはいわゆるEAPからTAPへの変換です。

Webアクセスなど、長い時間がかかることが見込まれる処理では、あるスレッドがそれにつきっきりになるとほかの処理が一切できず、ユーザーからはソフトがハングアップ(フリーズ)しているように見えてしまいます。そのような状態を防ぐために、非同期処理が必要になってきます。

EAP(Event-based Asynchronous Pattern)は、非同期プログラミングにおいて、処理の開始を行うメソッドと、終了時に呼ばれるイベントからなるパターンです。
例えば、WebClientクラスはこのパターンをサポートしており、Webからのダウンロードを非同期で実行することができます。
WebClient wc = new WebClient();

wc.DownloadStringCompleted += (s, e) => {
    Console.WriteLine(e.Result);
};

wc.DownloadStringAsync(new Uri("https://www.google.co.jp/"));
これはGoogleのトップページのソースを画面に表示するプログラムをEAPで実装したものです。イベントを使っているため、ダウンロードを開始する前にダウンロード後の処理を綴ったハンドラをイベントに登録しておき、その後に処理を開始するという書き方をしなければなりません。プログラムが上から下へ流れないので非常に見にくくなってしまいます。

一方で、TAP(Task-based Asynchronous Pattern)はTaskクラス/Task<T>クラスを使用した非同期パターンです。ライブラリとしてはこの説明だけなのですが、C#では言語機能でもこの非同期パターンに追従していて、C#5.0でasync/await構文が導入されました。これは非同期処理を同期処理であるかのような見た目で実行できる構文で、TAPとセットでこの構文を使うことによって非同期処理の記述がかなりすっきりできるようになりました。
WebClientクラスはこのパターンもサポートしており、上記のプログラムはTAPだと次のように書き換えられます。
WebClient wc = new WebClient();

string result = await wc.DownloadStringTaskAsync("https://www.google.co.jp/");
Console.WriteLine(result);
見てくださいこのシンプルさ。文字列をダウンロードして画面に出力する、というシンプルな流れのみを記述していますが、実際ちゃんと非同期で動作します。人類が古くから慣れ親しんでいる同期パターンと同じように非同期なプログラムを記述できる素晴らしい機能です。


さて、復習はこれくらいで本題に入ります。
WebClientのようにTAPに対応してくれているクラスならいいのですが、世の中にはそうではないクラスもたくさんあります。古いライブラリだったり、そもそももうちょっと汎用的なプログラムでEAPと呼ばれるものでは無かったり。

ズバリ、TaskCompletionSource<T>クラスを使います。
static async Task Main(string[] args)
{
    string result = await DownloadStringTaskAsync("https://www.google.co.jp/");
    Console.WriteLine(result);
}

static Task<string> DownloadStringTaskAsync(string url)
{
    var tcs = new TaskCompletionSource<string>();
    var wc = new WebClient();
    wc.DownloadStringCompleted += (sender, e) => {
        if(e.Error != null)
            tcs.TrySetException(e.Error);
        else if(e.Cancelled)
            tcs.TrySetCanceled();
        else
            tcs.TrySetResult(e.Result);

        wc.Dispose();
    };
    wc.DownloadStringAsync(new Uri(url));
    return tcs.Task;
}
結論から言うとこんな感じです。
TaskCompletionSourceクラスにはTaskプロパティがあり、このプロパティをawaitすることで、TaskCompletionSourceの終了待ちをすることができます。
ではどうやったらTaskCompletionSourceが終了するのかというと、TrySetResultが呼ばれたときになります。これを呼び出すことでawaitでの待機が終了し、パラメーターで渡していた値が結果として返されます。
エラーが発生したり、キャンセルが発生したときは、それぞれTrySetException、TrySetCanceledを呼び出せばOKです。


余談ですが、私がこれを欲しくなったのは、SerialPortクラスを触っているときでした。
そもそもシリアルポートは単なる全二重通信ですから、本来はWebClientのように「データを要求して、そのレスポンスが返ってくる」という使い方に限定されるようなものではありません。しかし、「何かしらのコマンドを送るとそれに対応したレスポンスがある」のような実装を行うシチュエーションはそれなりにあり、そのような場合ではTAPでクライアントを作ったほうがスッキリします。
というわけで、SerialPortクラスをTAPでラッピングしようとしたときに使ったのがこの手法でした。

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コントロールをGridViewColumnHeaderのContentとして使います。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のSelectedItemやSelectedIndexは単一選択しか想定されていないうえに、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で不便さを感じている皆さん、ぜひご活用ください!