エンジニアの備忘録

エンジニアの私が備忘録や思ったことをちょいちょい書いてます。

C# WPF-株価表示アプリ-02

環境

VisualStudio2019
OS:Windows10
プロセッサ:Core i7-10510U
メモリ:16GB
フレームワーク:4.7.2


前回までのあらすじ

完成図

f:id:dasuma20:20191118224137p:plain


あらすじ

株価表示アプリのModel側を説明しました。
今回はView/ViewModelを説明します。
まだ、見ていない方はご覧ください。
dasuma20.hatenablog.com


View

構成

f:id:dasuma20:20191118232713p:plain


App.xaml

<Application x:Class="StockData.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:StockData"
             Startup="StartUp_App"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             d1p1:Ignorable="d"
             xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006">
    <Application.Resources>
    </Application.Resources>
</Application>

スタートメソッドを変更するために下記を設定
Startup="StartUp_App"

詳しい説明は下記となります。
C# WPF-Tips-起動メソッドの変更 - エンジニアの備忘録


App.xaml.cs

namespace StockData
{
    public partial class App : Application
    {
        public void StartUp_App(object sender, StartupEventArgs e)
        {
            StockDataViewModel stockDataViewModel = new StockDataViewModel();
            StockDataView stockDataView = new StockDataView(stockDataViewModel)
            {
                DataContext = stockDataViewModel
            };
            stockDataView.Show();
        }
    }
}

StockDataView.xaml

メインXML
長いですが、各々説明します。

<Window x:Class="StockData.View.StockDataView"
        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:vali="clr-namespace:StockData.Validation"
        xmlns:local="clr-namespace:StockData"
        mc:Ignorable="d"
        Title="StockData" Height="400" Width="410"
        Icon="/img/icon.png">
    
    <StackPanel Orientation="Vertical"
                HorizontalAlignment="Center">
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    Margin="5">
            <Label Content="Start : "/>
            <TextBox 
                     Width="70"
                     HorizontalContentAlignment="Center"
                     VerticalContentAlignment="Center">
                <TextBox.Text>
                    <Binding Path="Condition.StartDate"
                             UpdateSourceTrigger="PropertyChanged"
                             Mode="TwoWay"
                             StringFormat="yyyy/M/d">
                        <Binding.ValidationRules>
                            <vali:DateValidationRule/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
                <TextBox.Style>
                    <Style TargetType="TextBox">
                        <Style.Triggers>
                            <Trigger Property="Validation.HasError" Value="true">
                                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                    Path=(Validation.Errors)[0].ErrorContent}"/>
                                <Setter Property="Background" Value="#ffeeff"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </TextBox.Style>
                <TextBox.InputBindings>
                    <KeyBinding Gesture="Enter" Command="{Binding FilterCommand}"/>
                </TextBox.InputBindings>
            </TextBox>
            <Label Content=" ~  End : "/>
            <TextBox 
                     Width="70"
                     HorizontalContentAlignment="Center"
                     VerticalContentAlignment="Center">
                <TextBox.Text>
                    <Binding Path="Condition.EndDate"
                             UpdateSourceTrigger="PropertyChanged"
                             Mode="TwoWay"
                             StringFormat="yyyy/M/d">
                        <Binding.ValidationRules>
                            <vali:DateValidationRule/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
                <TextBox.Style>
                    <Style TargetType="TextBox">
                        <Style.Triggers>
                            <Trigger Property="Validation.HasError" Value="true">
                                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                    Path=(Validation.Errors)[0].ErrorContent}"/>
                                <Setter Property="Background" Value="#ffeeff"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </TextBox.Style>
                <TextBox.InputBindings>
                    <KeyBinding Gesture="Enter" Command="{Binding FilterCommand}"/>
                </TextBox.InputBindings>
            </TextBox>
            <Button Content="ON" Command="{Binding FilterCommand}"
                    Margin="20, 0, 0, 0"/>
            <Button Content="Chart" Command="{Binding OpenChartCommand}"
                    Margin="20, 0, 0, 0"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal"
                    Margin="5">
            <DataGrid x:Name="StockGrid"
                      ItemsSource="{Binding StockList}" 
                      AutoGenerateColumns="False"
                      HeadersVisibility="Column"
                      MaxHeight="300"
                      CanUserAddRows="False"
                      CanUserDeleteRows="False"
                      CanUserResizeColumns="False"
                      CanUserResizeRows="False">
                <DataGrid.ColumnHeaderStyle>
                    <Style TargetType="DataGridColumnHeader">
                        <Setter Property="HorizontalContentAlignment" Value="Center"/>
                    </Style>
                </DataGrid.ColumnHeaderStyle>
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Date"
                                        Binding="{Binding Date, StringFormat='yyyy/MM/dd'}"
                                        Width="80"
                                        IsReadOnly="True">
                        <DataGridTextColumn.ElementStyle>
                            <Style TargetType="TextBlock">
                                <Setter Property="VerticalAlignment" Value="Center"/>
                                <Setter Property="HorizontalAlignment" Value="Center"/>
                            </Style>
                        </DataGridTextColumn.ElementStyle>
                    </DataGridTextColumn>
                    <DataGridTextColumn Header="Open"
                                        Binding="{Binding Open, StringFormat=F2}"
                                        Width="70"
                                        IsReadOnly="True">
                        <DataGridTextColumn.ElementStyle>
                            <Style TargetType="TextBlock">
                                <Setter Property="VerticalAlignment" Value="Center"/>
                                <Setter Property="HorizontalAlignment" Value="Center"/>
                            </Style>
                        </DataGridTextColumn.ElementStyle>
                    </DataGridTextColumn>
                    <DataGridTextColumn Header="Close"
                                        Binding="{Binding Close, StringFormat=F2}"
                                        Width="70"
                                        IsReadOnly="True">
                        <DataGridTextColumn.ElementStyle>
                            <Style TargetType="TextBlock">
                                <Setter Property="VerticalAlignment" Value="Center"/>
                                <Setter Property="HorizontalAlignment" Value="Center"/>
                            </Style>
                        </DataGridTextColumn.ElementStyle>
                    </DataGridTextColumn>
                    <DataGridTextColumn Header="High"
                                        Binding="{Binding High, StringFormat=F2}"
                                        Width="70"
                                        IsReadOnly="True">
                        <DataGridTextColumn.ElementStyle>
                            <Style TargetType="TextBlock">
                                <Setter Property="VerticalAlignment" Value="Center"/>
                                <Setter Property="HorizontalAlignment" Value="Center"/>
                            </Style>
                        </DataGridTextColumn.ElementStyle>
                    </DataGridTextColumn>
                    <DataGridTextColumn Header="Low"
                                        Binding="{Binding Low, StringFormat=F2}"
                                        Width="70"
                                        IsReadOnly="True">
                        <DataGridTextColumn.ElementStyle>
                            <Style TargetType="TextBlock">
                                <Setter Property="VerticalAlignment" Value="Center"/>
                                <Setter Property="HorizontalAlignment" Value="Center"/>
                            </Style>
                        </DataGridTextColumn.ElementStyle>
                    </DataGridTextColumn>
                </DataGrid.Columns>
            </DataGrid>
        </StackPanel>
    </StackPanel>
</Window>
アイコンの設定
Icon="/img/icon.png"
テキストボックスの設定

下記は"StartDate"、"EndDate"もほぼ同じ

<TextBox
         Width="70"
         HorizontalContentAlignment="Center"
         VerticalContentAlignment="Center">
    <TextBox.Text>
        <Binding Path="Condition.StartDate"
                 UpdateSourceTrigger="PropertyChanged"
                 Mode="TwoWay"
                 StringFormat="yyyy/M/d">
            <Binding.ValidationRules>
                <vali:DateValidationRule/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
    <TextBox.Style>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self},
        Path=(Validation.Errors)[0].ErrorContent}"/>
                    <Setter Property="Background" Value="#ffeeff"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
    <TextBox.InputBindings>
        <KeyBinding Gesture="Enter" Command="{Binding FilterCommand}"/>
    </TextBox.InputBindings>
</TextBox>


1.TextBox.Text
TextBoxに表示する文字をBindingさせる


2.UpdateSourceTrigger
変更通知を送るタイミングの設定
PropertyChanged:プロパティが変更されたタイミング
LostFocus:TextBoxからフォーカスが外れたタイミング


3.Mode
通知の方向性の設定
TwoWay:View→Model、Model→Viewの両方向で通知


4.StringFormat
Textのフォーマット


5.ValidationRules
独自のValidationルールの設定

<Binding.ValidationRules>
    <vali:DateValidationRule/>
</Binding.ValidationRules>

上記を使うために下記の設定が必要

xmlns:vali="clr-namespace:StockData.Validation"


6.Validationエラー内容をToolTipに表示

<TextBox.Style>
    <Style TargetType="TextBox">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self},
    Path=(Validation.Errors)[0].ErrorContent}"/>
                <Setter Property="Background" Value="#ffeeff"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</TextBox.Style>


7.EnterでCommand実行

<TextBox.InputBindings>
    <KeyBinding Gesture="Enter" Command="{Binding FilterCommand}"/>
</TextBox.InputBindings>
ボタンの設定
<Button Content="ON" Command="{Binding FilterCommand}"
              Margin="20, 0, 0, 0"/>

1.Content
ボタンに表示する文字列


2.Command
ボタンを押した時のCommand

DataGridの設定
<DataGrid x:Name="StockGrid"
          ItemsSource="{Binding StockList}"
          AutoGenerateColumns="False"
          HeadersVisibility="Column"
          MaxHeight="300"
          CanUserAddRows="False"
          CanUserDeleteRows="False"
          CanUserResizeColumns="False"
          CanUserResizeRows="False">
    <DataGrid.ColumnHeaderStyle>
        <Style TargetType="DataGridColumnHeader">
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
        </Style>
    </DataGrid.ColumnHeaderStyle>
    <DataGrid.Columns>
        <DataGridTextColumn Header="Date"
                            Binding="{Binding Date, StringFormat='yyyy/MM/dd'}"
                            Width="80"
                            IsReadOnly="True">
            <DataGridTextColumn.ElementStyle>
                <Style TargetType="TextBlock">
                    <Setter Property="VerticalAlignment" Value="Center"/>
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                </Style>
            </DataGridTextColumn.ElementStyle>
        </DataGridTextColumn>
        <DataGridTextColumn Header="Open"
                            Binding="{Binding Open, StringFormat=F2}"
                            Width="70"
                            IsReadOnly="True">
            <DataGridTextColumn.ElementStyle>
                <Style TargetType="TextBlock">
                    <Setter Property="VerticalAlignment" Value="Center"/>
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                </Style>
            </DataGridTextColumn.ElementStyle>
        </DataGridTextColumn>
        <DataGridTextColumn Header="Close"
                            Binding="{Binding Close, StringFormat=F2}"
                            Width="70"
                            IsReadOnly="True">
            <DataGridTextColumn.ElementStyle>
                <Style TargetType="TextBlock">
                    <Setter Property="VerticalAlignment" Value="Center"/>
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                </Style>
            </DataGridTextColumn.ElementStyle>
        </DataGridTextColumn>
        <DataGridTextColumn Header="High"
                            Binding="{Binding High, StringFormat=F2}"
                            Width="70"
                            IsReadOnly="True">
            <DataGridTextColumn.ElementStyle>
                <Style TargetType="TextBlock">
                    <Setter Property="VerticalAlignment" Value="Center"/>
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                </Style>
            </DataGridTextColumn.ElementStyle>
        </DataGridTextColumn>
        <DataGridTextColumn Header="Low"
                            Binding="{Binding Low, StringFormat=F2}"
                            Width="70"
                            IsReadOnly="True">
            <DataGridTextColumn.ElementStyle>
                <Style TargetType="TextBlock">
                    <Setter Property="VerticalAlignment" Value="Center"/>
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                </Style>
            </DataGridTextColumn.ElementStyle>
        </DataGridTextColumn>
    </DataGrid.Columns>
</DataGrid>

1.ItemsSource
表示するリストを設定する


2.AutoGenerateColumns
ON:リストに入っているデータを自動的にカラムに設定して表示
OFF:自動的にカラムを設定しない


3.HorizontalContentAlignment
ヘッダーの文字列の中央揃え

<DataGrid.ColumnHeaderStyle>
    <Style TargetType="DataGridColumnHeader">
        <Setter Property="HorizontalContentAlignment" Value="Center"/>
    </Style>
</DataGrid.ColumnHeaderStyle>


4.DataGridTextColumn
カラムの設定


5.DataGridTextColumn.ElementStyle
Cellの文字列の中央揃え

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

StockDataView.xaml.cs

メインXAMLのコードビハインド

namespace StockData.View
{
    public partial class StockDataView : Window
    {
        private StockDataViewModel _stockDataViewModel;

        public StockDataView(StockDataViewModel stockDataViewModel)
        {
            _stockDataViewModel = stockDataViewModel;

            InitializeComponent();
            Messenger.Default.Register<string>(this, "ErrorMsg", ErrorMsg);
        }

        public void ErrorMsg(string msg)
        {
            MessageBox.Show(msg, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }
}
メッセンジャー

メッセンジャーを使う場合はNugetからPackageのインストールが必要になります。
f:id:dasuma20:20191119222923p:plain

MvvmLight:v5.4.1.1
MvvmLightLibs:v5.4.1.1


Model or ViewModelからViewへ通知を送る方法

Messenger.Default.Register<string>(this, "ErrorMsg", ErrorMsg);
public void ErrorMsg(string msg)
{
    MessageBox.Show(msg, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}

DateValidationRule

独自のValidationルールの設定

namespace StockData.Validation
{
    public class DateValidationRule : ValidationRule
    {
        public DateValidationRule() { }

        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            if (value == null) return new ValidationResult(false, "値がnullです。");

            DateTime date;
            if (DateTime.TryParse(value.ToString(), out date))
            {
                return ValidationResult.ValidResult;
            }
            else
            {
                return new ValidationResult(false, "yyyy/MM/ddのフォーマットで入力してください。");
            }
        }
    }
}

ViewModel

StockDataViewModel.cs

StockDataViewと対になるStockDataViewModel

namespace StockData.ViewModel
{
    public class StockDataViewModel : BaseProperty
    {
        private Condition _condition;
        private ObservableCollection<Stock> _stockList = new ObservableCollection<Stock>();

        public StockDataViewModel()
        {
            //CSV読み込み
            CsvData.LoadStockList();

            //条件設定
            DateTime end = CsvData.GetStockList().AsEnumerable().Select(x => x.Date).Max();
            DateTime start = end.AddMonths(-1);
            _condition = new Condition(start, end);

            //初期表示用のFilter
            FilterData();
        }

        public void FilterData()
        {
            CsvData.GetStockList().AsParallel().ForAll(x => Condition.InRange(x));
            StockList = new ObservableCollection<Stock>(CsvData.GetStockList().Where(x => x.Visible).OrderByDescending(x => x.Date));
        }

        #region getter/setter
        public Condition Condition
        {
            get { return _condition; }
            set
            {
                if (_condition != value)
                {
                    _condition = value;
                    OnPropertyChanged("Condition");
                }
            }
        }
        public ObservableCollection<Stock> StockList
        {
            get { return _stockList; }
            set
            {
                if (_stockList != value)
                {
                    _stockList = value;
                    OnPropertyChanged("StockList");
                }
            }
        }
        #endregion

        #region command
        public ICommand FilterCommand
        {
            get { return new BaseICommand(FilterCommandCallBack); }
        }
        private void FilterCommandCallBack()
        {
            if (!Condition.Check())
            {
                Messenger.Default.Send("StartはEndより前の日付を入力してください。", "ErrorMsg");
            }
            else
            {
                FilterData();
            }
        }

        public ICommand OpenChartCommand
        {
            get { return new BaseICommand(OpenChartCommandCallBack); }
        }
        private void OpenChartCommandCallBack()
        {
            Messenger.Default.Send(string.Empty, "OpenChartView");
        }
        #endregion
    }
}

こちらは特段説明をする必要はないかと思います。


BaseICommand.cs

ICommandを継承したクラス

namespace StockData.ViewModel
{
    public class BaseICommand : ICommand
    {
        public delegate void DelegateAction();
        private DelegateAction _delegateAction;

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        private Func<bool> _canExecuteFunc;

        public BaseICommand(Action action)
            : this(action, null) { }

        public BaseICommand(Action action, Func<bool> canExe)
        {
            _delegateAction = new DelegateAction(action);
            _canExecuteFunc = canExe;
        }

        public bool CanExecute(object parameter)
        {
            if (_canExecuteFunc == null)
            {
                return true;
            }
            else
            {
                return _canExecuteFunc.Invoke();
            }
        }

        public void Execute(object parameter)
        {
            _delegateAction();
        }
    }
}

Commandの使い方は下記のブログをご覧ください。
C# WPF-Tips-CommandをButtonのトリガーに - エンジニアの備忘録


完成

これで全コードを載せ、説明しました。
わからない箇所がありましたら、コメント頂けますと
説明を追加します。


次回予告

データを表形式で表示するだけではやっぱりつまらないですね。
次は取得したデータをチャートにして表示させようと思います。