初めてのWPF 13日目

2019/02/13 WPF::Prism

GrandTotalを連動させたい

GrandTotalの表示

一番左側のボックスができれば後は楽勝ですが、子コレクションのメンバーのプロパティの変化(グリッド中の「個数/枚数」が変わった時)をViewModel側で取得するにはどうするのがいいのか分からない。
Web版MVVMだとこんなイメージ
非常に基本的な機能だと思うのですが、基本的過ぎてかサンプル見つけられず。

公式ドキュメント(いつの間にかlegacyの下に)みながらListをObservableCollectionに変え、MoneyCountのセッターでプロパティ名を外してみても、プロパティの変更が受け取れず。
OnPropertyChanged();
更にドキュメントに従いListCollectionViewでラップして見たらイベントは3つだけ。
ListCollectionViewEvent010.png

CollectionChangedは行の値を変えても移動しても走らず。CurrentChangedとCurrentChangingは行を移動するとイベントが走る。
private void SelectedItemChanged(object sender, EventArgs e)
{
    RaisePropertyChanged(nameof(GrandTotal));
    RaisePropertyChanged(nameof(Balance));
}
これで上(1円)から入力し、最後の行(10,000円)以外はきれいに連動。
10,000円の枚数を入力してから5,000円の行に戻るとちゃんと連動。
「仕様です」と言い張るか「最新の情報に更新」ボタンを付けるか?

スマホのショップアプリとかで依頼主いるような仕事だと許してもらえないでしょう?
xamarin shopping cartで検索したらMicrosoftさんにあった
ModelViewModel
合計値取得するのがプロパティじゃなくメソッドになっている。しかもLinq使わずforeach使ってる。

よりオフィシャルさのあるContosoApp
こちらはシンプルなModelラップしてからViewModelで使う場合の参考になりそう。

ObservableCollectionやListCollectionViewについては出納帳の方でまじめにやることにし、こちらはとりあえず仕様で済ます。

XAMLと戯れる

TextBox010.png

疲れた…。

数字をカンマ付きに変える

他に方法あるかも知れませんが一番最初に見つかったので…。
<TextBlock Text="{Binding GrandTotal, StringFormat={}{0:N0}, Mode=OneWay}" …

初めての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

初めてのWPF 11日目

2019/01/30 WPF::Prism
MainWindowのタイトルにモジュール名を追加したい。
NavigationComplete

Web屋さんの発想かも。
リージョン作成し、モジュール側でUserControlを作って差込にいけば出来ることは出来るはず。
たかがタイトルごときに考えただけで面倒過ぎて試す気すら起こらず。
メニュー用にモジュールの配列持っているしそこから取り出したい。
公式サンプル眺めていたら、Navigation Callbackが使えそう。
private void NavigationComplete(NavigationResult result)
{
    //System.Windows.MessageBox.Show(String.Format(result.Context.Uri.ToString()));

    // せっかくなのでDrawerメニューの開閉もこちらに移動
    MainMenuIsOpen = false;

    //_moduleTitleだとなぜかViewに反映されない(ポップアップには表示される…)
    ModuleTitle = MainMenuItems.Where(m => m.NavigatePath == result.Context.Uri.ToString()).FirstOrDefault().Title;
    System.Windows.MessageBox.Show(_moduleTitle);
}
若干不安(^^;)。

初めてのWPF 10日目

2019/01/26 WPF::Prism

PrismのCustomPopupを使う

TreasurerHelper.Infrastructureに参照追加

もともと通常のライブラリ用テンプレートで作成したので、足りないものが一杯。
コントロール専用プロジェクトを作ることも考えましたが、TreasurerHelper.InfrastructureにControlsフォルダを作成しました。
参照追加

別プロジェクトのUserControlを参照する

最初コントロール専用のプロジェクトを作成した時に、非常に親切なコメントが添えられていたのでそれほどはまらずにすみました(^^;)。
NameSpaceはアセンブリ名を付ける必要があるとのこと。
<UserControl ・・・
xmlns:cc="clr-namespace:TreasurerHelper.Infrastructure.Controls;assembly=TreasurerHelper.Infrastructure"
・・・
無事表示されました。
ユーザーコントロール

IInteractionRequestAwareを実装

とりあえずサンプルをそのままコピー。
カスタムポップアップ

同意ボタン押下後

後はMaterialDesignのDialogを適用するだけ!

のはずがDialogが単なるテーマではなく結構高機能で大掛かり。
Prismを取るかMaterialDesignを取るかで悩む。
やはり機能はPrismで通したい、ということでMaterialDesignのDialogは諦める。
似たようなデザインにするにはCustomPopupをMaterialDesignのCardに載せ、ウインドウの枠を消す。
Cardに載せるのは簡単
<Grid Background="Transparent">
     <md:Card Margin="10">
         <StackPanel Margin="16">
            <TextBlock Text="{Binding Title}" FontSize="24" HorizontalAlignment="Center"/>
            <TextBlock Text="{Binding Content}" Margin="10" />
            <Button Margin="25" Click="Button_Click">Accept</Button>
         </StackPanel>
    </md:Card>
</Grid>
Windowの枠を消す設定は調べたら直ぐ分かった
<Setter Property="WindowStyle" Value="None" />
<Setter Property="AllowsTransparency" Value="True"/>
だが対象のWindowがどこにあるか分からない…
CustomPopupをUserControlからWindowに変えたらルートじゃないのにWindow使うなと怒られる。
ここからPopupWindowActionとWindowStyleでこちらにたどりつく。
<i:Interaction.Triggers>
    <prism:InteractionRequestTrigger SourceObject="{Binding FileOpenRequest}">
        <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True">
<prism:PopupWindowAction.WindowStyle>
    <Style TargetType="Window">
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="WindowStyle" Value="None" />
        <Setter Property="ResizeMode" Value="NoResize" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="ShowInTaskbar" Value="False"/>
        <Setter Property="AllowsTransparency" Value="True"/>
    </Style>
</prism:PopupWindowAction.WindowStyle>
<prism:PopupWindowAction.WindowContent>
                <cc:CustomPopupView />
            </prism:PopupWindowAction.WindowContent>
        </prism:PopupWindowAction>
    </prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
WindowStyle設定後


周りはそれっぽくなりましたが、思っていたのとはちょっと違う。
コントロールは使いまわししたいからコンテンツによって自動でサイズ変わって欲しい、と思ったらちょうどそれらしいタグが。
<Setter Property="SizeToContent" Value="WidthAndHeight" />
追加したら思い描いていたようなイメージに!
SizeToContent追加

でも毎回大量のタグをコピペするのは嫌
結局最初のリンクにもどりPopupWindowActionのGetWindowを上書きすることに。
PopupChildWindowActionという名前から丸ごといただき、return wrapperWindow;
の前にWindowStyleの設定を追加。
wrapperWindow.WindowStyle = System.Windows.WindowStyle.None;
wrapperWindow.AllowsTransparency = true;
wrapperWindow.Background = Brushes.Transparent;
wrapperWindow.ShowInTaskbar = false;
wrapperWindow.ResizeMode = ResizeMode.NoResize;
wrapperWindow.SizeToContent = SizeToContent.WidthAndHeight;
// ↑の部分を追加
return wrapperWindow;
実行後のイメージはまったく同じですが、呼び出し部分が随分すっきりしました。
<i:Interaction.Triggers>
    <prism:InteractionRequestTrigger SourceObject="{Binding FileOpenRequest}">
        <cc:PopupChildWindowAction IsModal="True" CenterOverAssociatedObject="True">
            <prism:PopupWindowAction.WindowContent>
                <cc:CustomPopupView />
            </prism:PopupWindowAction.WindowContent>
        </cc:PopupChildWindowAction>
    </prism:InteractionRequestTrigger>
</i:Interaction.Triggers>

初めてのWPF 9日目

やりたいこと

起動時に押せるボタン:ファイルオープン、CSVインポート
起動時にアクティブなボタン


データ入力後に押せるボタン:保存、印刷プレビュー、印刷
データ入力後にアクティブなボタン


データを空にすると起動時と同じ状態
データを空にするとアクティブなボタン

MaterialDesignToolBarを使う

View側は楽勝。TextBoxやCheckBoxはスタイルの指定すら不要。
ToolBarのRegionをどうするかで迷いましたが、とりあえずModule側の基本画面に表示することに。
<ToolBarTray DockPanel.Dock="Top">
    <ToolBar Style="{DynamicResource MaterialDesignToolBar}" ClipToBounds="True">
        <Button ToolTip="ファイルを開く" Command="{Binding FileOpenCommand}">
            <materialDesign:PackIcon Kind="Folder" />
        </Button>
        <Button ToolTip="データを保存" Command="{Binding FileSaveCommand}">
            <materialDesign:PackIcon Kind="Floppy" />
        </Button>
        <Button ToolTip="CSVファイルをインポート" Command="{Binding CsvImportCommand}">
            <materialDesign:PackIcon Kind="FileDelimited" />
        </Button>
        <Separator />
        <Button ToolTip="印刷プレビュー" Command="{Binding PrintPreviewCommand}">
            <materialDesign:PackIcon Kind="FileFind" />
        </Button>
        <Button ToolTip="印刷" Command="{Binding PrintCommand}">
            <materialDesign:PackIcon Kind="Printer" />
        </Button>
    </ToolBar>
</ToolBarTray>

DelegateCommandを使う

NameSpace追加

using Prism.Commands;

アイコンの数だけDelegateCommand??

public DelegateCommand FileOpenCommand { get; private set; } 
public DelegateCommand FileSaveCommand { get; private set; }
public DelegateCommand CsvImportCommand { get; private set; }
public DelegateCommand PrintPreviewCommand { get; private set; }
public DelegateCommand PrintCommand { get; private set; }

ボタンのCanExecuteを制御

チェックボックスで制御するのはサンプル通りなので直ぐに出来ましたが、保存・印刷可能なコンテンツがあるかどうかで制御したいわけでTextBoxが空かどうかで制御しようとしたらどうしたらいいのか分からず。
テキストボックスの値が変わった時にRaiseCanExecuteChangedを呼んでも動きましたが、最終的にはObservesPropertyを使ってとりあえず動いたよ。
// bootをプロパティに変える
private bool HaveData()
{
    return !string.IsNullOrEmpty(InputData);
}
private bool NoData()
{
    // 微妙ですが…
    return string.IsNullOrEmpty(InputData);
}

public DelegateCommand FileOpenCommand { get; private set; } 
public DelegateCommand FileSaveCommand { get; private set; }
public DelegateCommand CsvImportCommand { get; private set; }
public DelegateCommand PrintPreviewCommand { get; private set; }
public DelegateCommand PrintCommand { get; private set; }

public CashbookViewModel()
{
    // ObservesPropertyでInputDataの変化を監視し、NoDataを再評価する感じ?
    FileOpenCommand = new DelegateCommand(FileOpen, NoData).ObservesProperty(() => InputData);
    FileSaveCommand = new DelegateCommand(FileSave, HaveData).ObservesProperty(() => InputData);
    CsvImportCommand = new DelegateCommand(CsvImport, NoData).ObservesProperty(() => InputData);
    PrintPreviewCommand = new DelegateCommand(PrintPreview, HaveData).ObservesProperty(() => InputData);
    PrintCommand = new DelegateCommand(Print, HaveData).ObservesProperty(() => InputData); 
}
監視対象のフィールドが複数ある場合はチェインでつなぐことも可能なようです。
OK キャンセル 確認 その他