初めてのWPF 12日目

2019/02/01 WPF::Prism
そろそろ真面目にBindableBaseについて調べる(^^;)。
公式ドキュメントはここですが、最新ソースと比べたら古そう。
public abstract class BindableBase : INotifyPropertyChanged
{
    // プロパティの値が変化した時のイベント
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual bool SetProperty<T>(ref T storage, T value,
              [CallerMemberName] string propertyName = null)
    {...}

    protected virtual bool SetProperty<T>(ref T storage, T value,
      Action onChanged,[CallerMemberName] string propertyName = null)
    {...}

    protected void RaisePropertyChanged([CallerMemberName]string propertyName = null)
    {...}

    // 廃止予定 代わりに新しいRaisePropertyChangedメソッドを使うように
    protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
    {...}

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
    {...}

    // 廃止予定
    // 代わりにRaisePropertyChanged(nameof(PropertyName))を使うように
    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {...}
}

Model(DomainObject)もINotifyPropertyChangedを実装しろとか思っていたのとは違う。
ModelではINotifyDataErrorInfoも実装。これは納得。

過去のサンプルをつついてみていると、BindableBaseが登場したのはPrism5から。
Prism5のソース附属のサンプルにはDomainObjectが確認できず。
DomainObjectがBindableBaseに変わったのかと思ったのですが、現在のサンプル見るとViewModelではBindableBaseを継承し、ModelではINotifyPropertyChangedを継承とちゃんと使い分けられている。
ハードル上がってきました。

MVVMパターン

クラスを役割に応じて3つに分ける
クラス役割
viewUIとUIのロジック。XAMLだけでは表現しきれない複雑なアニメーションのロジック等はここ。InitializeComponentだけが好ましい。
view modelプレゼンテーションロジックと状態
BindableBaseを継承していることが多い。
modelアプリケーションのビジネスロジックとデータ
INotifyPropertyChangedとINotifyDataErrorInfoを実装しておくとよろしい

Model

親クラス

公式サンプルにはなぜかMVVMメインのサンプルが見当たらず。
公式ドキュメントが最低限メンテナンスされていると信じ、INotifyPropertyChangedとINotifyDataErrorInfoを実装したModelBaseを作る。
Prism4のソースと一緒に配布されているサンプルにDomainObject.csが含まれていてしかもDataAnnotationsが使われています(なぜPrism5で消えた?)。
これをメインに同じようなことを考えている人を見つけて一部いただき、BindableBaseと同じような書き方が出来るように変更。
(自信ないです。責任持てないです。そのうちこっそり変わっているかも)
using Prism.Mvvm;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;

namespace TreasurerHelper.Infrastructure.Models
{
    /// <summary>
    /// Base domain object class.
    /// </summary>
    /// <remarks>
    /// Provides support for implementing <see cref="INotifyPropertyChanged"/> and 
    /// implements <see cref="INotifyDataErrorInfo"/> using <see cref="ValidationAttribute"/> instances
    /// on the validated properties.
    /// </remarks>
    public abstract class DomainObject : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        private ErrorsContainer<ValidationResult> errorsContainer;

        /// <summary>
        /// Initializes a new instance of the <see cref="DomainObject"/> class.
        /// </summary>
        protected DomainObject()
        {
        }

        /// <summary>
        /// sets the storage to the value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="storage"></param>
        /// <param name="value"></param>
        /// <param name="propertyName"></param>
        /// <returns>True if the value was changed, false if the existing value matched the desired value.</returns>
        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            ValidateProperty(propertyName, value);

            if (object.Equals(storage, value)) return false;

            storage = value;
            OnPropertyChanged(propertyName);

            return true;
        }

        /// <summary>
        /// Notifies listeners that a property value has changed.
        /// </summary>
        /// <param name="propertyName">Name of the property used to notify listeners. This
        /// value is optional and can be provided automatically when invoked from compilers
        /// that support <see cref="CallerMemberNameAttribute"/>.</param>
        protected void OnPropertyChanged(string propertyName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }


        /// <summary>
        /// Event raised when a property value changes.
        /// </summary>
        /// <seealso cref="INotifyPropertyChanged"/>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Event raised when the validation status changes.
        /// </summary>
        /// <seealso cref="INotifyDataErrorInfo"/>
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        /// <summary>
        /// Gets the error status.
        /// </summary>
        /// <seealso cref="INotifyDataErrorInfo"/>
        public bool HasErrors
        {
            get { return this.ErrorsContainer.HasErrors; }
        }

        /// <summary>
        /// Gets the container for errors in the properties of the domain object.
        /// </summary>
        protected ErrorsContainer<ValidationResult> ErrorsContainer
        {
            get
            {
                if (this.errorsContainer == null)
                {
                    this.errorsContainer =
                        new ErrorsContainer<ValidationResult>(pn => this.RaiseErrorsChanged(pn));
                }

                return this.errorsContainer;
            }
        }

        /// <summary>
        /// Returns the errors for <paramref name="propertyName"/>.
        /// </summary>
        /// <param name="propertyName">The name of the property for which the errors are requested.</param>
        /// <returns>An enumerable with the errors.</returns>
        /// <seealso cref="INotifyDataErrorInfo"/>
        public IEnumerable GetErrors(string propertyName)
        {
            return this.errorsContainer.GetErrors(propertyName);
        }

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">The name of the changed property.</param>
        protected void RaisePropertyChanged(string propertyName)
        {
            this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="e">The argument for the event.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            this.PropertyChanged?.Invoke(this, e);
        }

        /// <summary>
        /// Validates <paramref name="value"/> as the value for the property named <paramref name="propertyName"/>.
        /// </summary>
        /// <param name="propertyName">The name of the property.</param>
        /// <param name="value">The value for the property.</param>
        protected void ValidateProperty(string propertyName, object value)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ArgumentNullException("propertyName");
            }

            this.ValidateProperty(new ValidationContext(this, null, null) { MemberName = propertyName }, value);
        }

        /// <summary>
        /// Validates <paramref name="value"/> as the value for the property specified by 
        /// <paramref name="validationContext"/> using data annotations validation attributes.
        /// </summary>
        /// <param name="validationContext">The context for the validation.</param>
        /// <param name="value">The value for the property.</param>
        protected virtual void ValidateProperty(ValidationContext validationContext, object value)
        {
            if (validationContext == null)
            {
                throw new ArgumentNullException("validationContext");
            }

            List<ValidationResult> validationResults = new List<ValidationResult>();
            Validator.TryValidateProperty(value, validationContext, validationResults);

            this.ErrorsContainer.SetErrors(validationContext.MemberName, validationResults);
        }

        /// <summary>
        /// Raises the <see cref="ErrorsChanged"/> event.
        /// </summary>
        /// <param name="propertyName">The name of the property which changed its error status.</param>
        protected void RaiseErrorsChanged(string propertyName)
        {
            this.OnErrorsChanged(new DataErrorsChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Raises the <see cref="ErrorsChanged"/> event.
        /// </summary>
        /// <param name="e">The argument for the event.</param>
        protected virtual void OnErrorsChanged(DataErrorsChangedEventArgs e)
        {
            this.ErrorsChanged?.Invoke(this, e);
        }
    }
}

Model本体

public class CountItem : DomainObject
{
    /// <summary>
    /// お金の種類(1000円とか10円とか)
    /// </summary>
    private string _moneyType;
    public string MoneyType {
        get { return _moneyType; }
        set {
            SetProperty(ref _moneyType, value);
        }
    }

    /// <summary>
    /// 種類ごとの金額
    /// </summary>
    private decimal _mondeyValue;
    public decimal MondeyValue {
        get { return _mondeyValue; }
        set {  SetProperty(ref _mondeyValue, value);
        }
    }

    /// <summary>
    /// 個数、枚数
    /// </summary>
    private int _moneyCount = 0;
    [Range(0,99999,ErrorMessage = "マイナスはダメです")]
    public int MoneyCount {
        get { return _moneyCount; }
        set {
            SetProperty(ref _moneyCount, value);
            OnPropertyChanged(nameof(AmountByType));  // 他に書き方無いのか?
        }
    }

    /// <summary>
    /// 種類ごとの金額を返す
    /// </summary>
    /// <returns>種類毎の金額</returns>
    public decimal AmountByType
    {
        get { return MondeyValue * MoneyCount; }
    }
}

ViewModel

BindableBase継承してますが、今回はListは固定なので必要ないかも。
public class CashCalculatorViewModel : BindableBase
{
    public List<CountItem> CountItems { get; set; }

    public CashCalculatorViewModel()
    {
        CountItems = new List<CountItem> {
            new CountItem{MoneyType = "1円", MondeyValue = 1, MoneyCount = 0 },
            new CountItem{MoneyType= "5円", MondeyValue = 5, MoneyCount = 0},
            new CountItem{MoneyType= "10円", MondeyValue = 10, MoneyCount = 0},
            new CountItem{MoneyType= "50円", MondeyValue = 50, MoneyCount = 0},
            new CountItem{MoneyType= "100円", MondeyValue = 100, MoneyCount = 0},
            new CountItem{MoneyType= "500円", MondeyValue = 500, MoneyCount = 0},
            new CountItem{MoneyType= "1,000円", MondeyValue = 1000, MoneyCount = 0},
            new CountItem{MoneyType= "5,000円", MondeyValue = 5000, MoneyCount = 0},
            new CountItem{MoneyType= "10,000円", MondeyValue = 10000, MoneyCount = 0}
        };
    }
}

View

DataGridTextColumnを右寄せ

<DataGridTextColumn.ElementStyle>
    <Style TargetType="TextBlock">
        <Setter Property="HorizontalAlignment" Value="Right" />
    </Style>
</DataGridTextColumn.ElementStyle>

DataGridTextColumnを読取専用に

<DataGridTextColumn  IsReadOnly="True"
…

DataGridTextColumn間の値の連動

編集する側
<DataGridTextColumn Binding="{Binding MoneyCount, Mode=TwoWay}" Header="個数/枚数">
自動で値が変わる側
<DataGridTextColumn Binding="{Binding AmountByType, Mode=OneWay}" IsReadOnly="True" Header="合計金額">

Validation

View側では何もしなくてもちゃんとアノテーションで指定したエラーメッセージが表示された。
と思ったら代入しちゃ駄目なものを代入してる…。
validation010.png

OK キャンセル 確認 その他