Xamarin.Forms 資産管理 サンプルコード

以前にUWPで作成したシステムをスマホアプリに置き換えました。
置き換えるにあたり、Xamarin.Formsというフレームワークを使っています。
Xamarin.FormとUWPのどちらもXAMLとC#で作成するので、Xamarin.FormsとUWPのコードを見比べることで、これまでデスクトップ開発の経験のみの方がスマホアプリを作る時の参考になればと思っています。

知人から依頼があり、UWPのコードからFIRE可能年度計算タブを追加しましたが、その他の基本的なところは同様です。

私はMacを持っていないため、iOSの動作確認はできていません。
iOSでも動くと思いますが、ご了承ください。

完成画面

まず完成画面です。
簡易計算タブと詳細計算タブがあり、簡易計算タブの中に以下3つのタブがある形となっています。

  1. 必要積立金計算タブ
  2. 必要利回り計算タブ
  3. FIRE可能年度計算タブ

サンプルコードのため、詳細計算タブと必要利回り計算タブの中身は「作成中です」と表示しています。

画面の操作を録画したものを以下に貼りました。
エミュレーターで動かしているため動きが少し重いですがご参考ください。

フォルダ構成

フォルダ構成は次のようになります。
AndroidとiOS用のプロジェクトがありますが、ソリューションを作成した時のまま変更していないので、AssetManagementXamarinプロジェクトのファイルのみ解説していきます。

Xamarin.Forms-資産管理システム-フォルダ構成

ソリューション直下

まず、ソリューション直下に配置されたファイルを説明していきます。

App.xaml

アプリ全体で使いたいデザインの設定をしています。
具体的には文字やボタンの色を決めています。

<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:dg="clr-namespace:Xamarin.Forms.DataGrid;assembly=Xamarin.Forms.DataGrid" 
             x:Class="AssetManagementXamarin.App">
    <!--
        Define global resources and styles here, that apply to all pages in your app.
    -->
    <Application.Resources>
        <ResourceDictionary>
            <Color x:Key="Primary">#2196F3</Color>
            <Style TargetType="Button">
                <Setter Property="TextColor" Value="White"></Setter>
                <Setter Property="VisualStateManager.VisualStateGroups">
                    <VisualStateGroupList>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="#332196F3" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateGroupList>
                </Setter>
            </Style>
            <Style TargetType="Label">
                <Setter Property="TextColor" Value="Black"/>
            </Style>
            <Style TargetType="Entry">
                <Setter Property="TextColor" Value="Black"/>
            </Style>
        </ResourceDictionary>        
    </Application.Resources>
</Application>

AppShell.xaml

大枠となるページです。
このページ内でタブが切り替わるようになっています。

<?xml version="1.0" encoding="UTF-8"?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms" 
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:AssetManagementXamarin.Views"
       Title="AssetManagementXamarin"
       x:Class="AssetManagementXamarin.AppShell">

    <!--
        The overall app visual hierarchy is defined here, along with navigation.
    
        https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/
    -->

    <Shell.Resources>
        <ResourceDictionary>
            <Style x:Key="BaseStyle" TargetType="Element">
                <Setter Property="Shell.BackgroundColor" Value="{StaticResource Primary}" />
                <Setter Property="Shell.ForegroundColor" Value="White" />
                <Setter Property="Shell.TitleColor" Value="White" />
                <Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
                <Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
                <Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource Primary}" />
                <Setter Property="Shell.TabBarForegroundColor" Value="White"/>
                <Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
                <Setter Property="Shell.TabBarTitleColor" Value="White"/>
            </Style>
            <Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
            <Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
        </ResourceDictionary>
    </Shell.Resources>

    <TabBar>
        <Tab Title="簡易計算" Icon="icon_about.png">
            <ShellContent Title="必要積立金計算" Route="CalcRequiredMoneyPage" ContentTemplate="{DataTemplate local:CalcRequiredMoneyPage}" />
            <ShellContent Title="必要利回り計算" Route="CalcDividendRatePage" ContentTemplate="{DataTemplate local:CalcDividendRatePage}" />
            <ShellContent Title="FIRE可能年度計算" Route="CalcGoalYearPage" ContentTemplate="{DataTemplate local:CalcGoalYearPage}" />
        </Tab>
        <Tab Title="詳細計算" Icon="icon_feed.png">
            <ShellContent ContentTemplate="{DataTemplate local:DetailCalcPage}" />
        </Tab>
    </TabBar>
</Shell>

Views

CalcRequiredMoneyPage.xaml

必要積立金計算タブのレイアウトです。
ユーザー入力を受け付けるコントロールは、 入力単位1行分となる「項目名」、「入力欄」、「単位」をInputControlとしてまとめています。
InputControlについては後述します。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AssetManagementXamarin.Views.CalcRequiredMoneyPage"
             xmlns:vm="clr-namespace:AssetManagementXamarin.ViewModels"
             xmlns:ctrl="clr-namespace:AssetManagementXamarin.Controls"
             xmlns:dg="clr-namespace:Xamarin.Forms.DataGrid;assembly=Xamarin.Forms.DataGrid" 
             Title="{Binding Title}">
    
    <ContentPage.BindingContext>
        <vm:CalcRequiredMoneyViewModel />
    </ContentPage.BindingContext>

    <Grid>  
        <ScrollView>
            <StackLayout Margin="15,15">
                <ctrl:InputControl x:Name="InputStartYearControl" ItemText="投資を開始する年" UnitText="年"
                                   InputValue="{Binding StartYear, Mode=TwoWay}" HasError="{Binding HasErrorGoalRetireYear, Mode=OneWay}" />
                <ctrl:InputControl x:Name="InputStartAssetControl" ItemText="投資開始時の資産" UnitText="万円"
                                   InputValue="{Binding StartAsset, Mode=TwoWay}"/>
                <ctrl:InputControl x:Name="InputGoalDividendControl" ItemText="1年間で欲しい配当額" UnitText="万円"
                                   InputValue="{Binding GoalDividend, Mode=TwoWay}" HasError="{Binding HasErrorGoalDividend, Mode=OneWay}"/>
                <ctrl:InputControl x:Name="InputGoalRetireYearControl" ItemText="FIRE達成したい年" UnitText="年"
                                   InputValue="{Binding GoalRetireYear, Mode=TwoWay}" HasError="{Binding HasErrorGoalRetireYear, Mode=OneWay}"/>
                <ctrl:InputControl x:Name="InputExpectedDividendRateControl" ItemText="想定年利" UnitText="%"
                                   InputValue="{Binding ExpectedDividendRate, Mode=TwoWay}" HasError="{Binding HasErrorExpectedDividendRate, Mode=OneWay}"/>
                <Button Text="計算" Command="{Binding ButtonClickCommand}" WidthRequest="150" HeightRequest="70" Margin="0,10"
                        HorizontalOptions="Center" VerticalOptions="Center" Background="LightBlue" TextColor="White"/>
                <StackLayout x:Name="resultPanel" IsVisible="{Binding ResultVisibility, Mode=OneWay}" VerticalOptions="FillAndExpand" Orientation="Vertical">
                    <Label Text="シミュレーション結果" HorizontalOptions="Center" Margin="0,8,0,0" FontSize="Title"/>
                    <Label Text="必要年間積み立て額" HorizontalTextAlignment="Center" Margin="0,10,0,3" FontSize="Medium"/>
                    <StackLayout Orientation="Horizontal" HorizontalOptions="Center">
                        <StackLayout.Resources>
                            <Style TargetType="Label">
                                <Setter Property="FontAttributes" Value="Bold"/>
                                <Setter Property="HorizontalTextAlignment" Value="Center"/>
                                <Setter Property="FontSize" Value="Large"/>
                            </Style>
                        </StackLayout.Resources>
                        <Label Text="{Binding ResultValue, Mode=OneWay}"/>
                        <Label Text="万円"/>
                    </StackLayout>
                    <dg:DataGrid x:Name="ResultGrid" ItemsSource="{Binding GridData, Mode=OneWay}" RowHeight="50" HeaderHeight="50"
                                 BorderColor="#CCCCCC" HeaderBackground="LightSkyBlue" HeaderTextColor="White" Margin="0,5,0,15" VerticalOptions="FillAndExpand">
                        <dg:DataGrid.Columns>
                            <dg:DataGridColumn Title="年" PropertyName="Year" Width="*"/>
                            <dg:DataGridColumn Title="資産額(万円)" PropertyName="TotalAsset" Width="2*"/>
                            <dg:DataGridColumn Title="年間配当(万円)" PropertyName="Dividend" Width="2*"/>
                        </dg:DataGrid.Columns>
                        <dg:DataGrid.RowsBackgroundColorPalette>
                            <dg:PaletteCollection>
                                <Color>#F2F2F2</Color>
                                <Color>#FFFFFF</Color>
                            </dg:PaletteCollection>
                        </dg:DataGrid.RowsBackgroundColorPalette>
                    </dg:DataGrid>
                </StackLayout>
            </StackLayout>
        </ScrollView>
    </Grid>

</ContentPage>

CalcRequiredMoneyPage.xaml.cs

コードビハインドです。
MVVMパターンで作成するため、コードビハインドには基本的に何も書きません。

using Xamarin.Forms;

namespace AssetManagementXamarin.Views
{
    public partial class CalcRequiredMoneyPage : ContentPage
    {
        public CalcRequiredMoneyPage()
        {
            InitializeComponent();
        }
    }
}

CalcGoalYearPage.xaml

FIRE可能年度計算タブのレイアウトになります。
レイアウトは必要積立金計算タブのCalcRequiredMoneyPage.xamlとほとんど同じ作りです。

ほとんど同じ作りですが、必要積立金計算タブを作った後でFIRE可能年度計算タブも作ってほしいと依頼があったため、時間の都合で共通化していません。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AssetManagementXamarin.Views.CalcGoalYearPage"
             xmlns:vm="clr-namespace:AssetManagementXamarin.ViewModels"
             xmlns:ctrl="clr-namespace:AssetManagementXamarin.Controls"
             xmlns:dg="clr-namespace:Xamarin.Forms.DataGrid;assembly=Xamarin.Forms.DataGrid" 
             Title="{Binding Title}">

    <ContentPage.BindingContext>
        <vm:CalcGoalYearViewModel/>
    </ContentPage.BindingContext>

    <Grid>
        <ScrollView>
            <StackLayout Margin="15,15">
                <ctrl:InputControl x:Name="InputStartYearControl" ItemText="投資を開始する年" UnitText="年"
                                   InputValue="{Binding StartYear, Mode=TwoWay}"/>
                <ctrl:InputControl x:Name="InputStartAssetControl" ItemText="投資開始時の資産" UnitText="万円"
                                   InputValue="{Binding StartAsset, Mode=TwoWay}"/>
                <ctrl:InputControl x:Name="InputAnualAffordableCashControl" ItemText="年間可能投資額" UnitText="万円"
                                   InputValue="{Binding AnualAffordableCash, Mode=TwoWay}" HasError="{Binding HasErrorAnualAffordableCash, Mode=OneWay}"/>
                <ctrl:InputControl x:Name="InputGoalDividendControl" ItemText="1年間で欲しい配当額" UnitText="万円"
                                   InputValue="{Binding GoalDividend, Mode=TwoWay}" HasError="{Binding HasErrorGoalDividend, Mode=OneWay}"/>
                <ctrl:InputControl x:Name="InputExpectedDividendRateControl" ItemText="想定年利" UnitText="%"
                                   InputValue="{Binding ExpectedDividendRate, Mode=TwoWay}" HasError="{Binding HasErrorExpectedDividendRate, Mode=OneWay}"/>
                <Grid Margin="0,3">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="4*"/>
                        <ColumnDefinition Width="5*"/>
                    </Grid.ColumnDefinitions>
                    <Label Grid.Column="0" Text="配当を再投資する"/>
                    <CheckBox Grid.Column="1" IsChecked="{Binding IsCompound, Mode=TwoWay}" BackgroundColor="White" Color="CornflowerBlue"/>
                </Grid>
                <Button Text="計算" Command="{Binding ButtonClickCommand}" WidthRequest="150" HeightRequest="70" Margin="0,10"
                        HorizontalOptions="Center" VerticalOptions="Center" Background="LightBlue" TextColor="White"/>
                <StackLayout x:Name="resultPanel" IsVisible="{Binding ResultVisibility, Mode=OneWay}" VerticalOptions="FillAndExpand" Orientation="Vertical">
                    <Label Text="シミュレーション結果" HorizontalOptions="Center" Margin="0,8,0,0" FontSize="Title"/>
                    <Label Text="FIRE可能年度計算" HorizontalTextAlignment="Center" Margin="0,10,0,3" FontSize="Medium"/>
                    <StackLayout Orientation="Horizontal" HorizontalOptions="Center">
                        <StackLayout.Resources>
                            <Style TargetType="Label">
                                <Setter Property="FontAttributes" Value="Bold"/>
                                <Setter Property="HorizontalTextAlignment" Value="Center"/>
                                <Setter Property="FontSize" Value="Large"/>
                            </Style>
                        </StackLayout.Resources>
                        <Label Text="{Binding ResultValue, Mode=OneWay}"/>
                        <Label Text="年"/>
                    </StackLayout>
                    <dg:DataGrid x:Name="ResultGrid" ItemsSource="{Binding GridData, Mode=OneWay}" RowHeight="50" HeaderHeight="50"
                                 BorderColor="#CCCCCC" HeaderBackground="LightSkyBlue" HeaderTextColor="White" Margin="0,5,0,15" VerticalOptions="FillAndExpand">
                        <dg:DataGrid.Columns>
                            <dg:DataGridColumn Title="年" PropertyName="Year" Width="*"/>
                            <dg:DataGridColumn Title="資産額(万円)" PropertyName="TotalAsset" Width="2*"/>
                            <dg:DataGridColumn Title="年間配当(万円)" PropertyName="Dividend" Width="2*"/>
                        </dg:DataGrid.Columns>
                        <dg:DataGrid.RowsBackgroundColorPalette>
                            <dg:PaletteCollection>
                                <Color>#F2F2F2</Color>
                                <Color>#FFFFFF</Color>
                            </dg:PaletteCollection>
                        </dg:DataGrid.RowsBackgroundColorPalette>
                    </dg:DataGrid>
                </StackLayout>
            </StackLayout>
        </ScrollView>
    </Grid>
</ContentPage>

CalcGoalYearPage.xaml.cs

コードビハインドです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace AssetManagementXamarin.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class CalcGoalYearPage : ContentPage
    {
        public CalcGoalYearPage()
        {
            InitializeComponent();
        }
    }
}

CalcDividendRatePage.xaml

必要利回り計算タブのレイアウトになります。
知人が使うためのサンプルアプリのため、「作成中です」とのみ表示しています。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AssetManagementXamarin.Views.CalcDividendRatePage"
             xmlns:vm="clr-namespace:AssetManagementXamarin.ViewModels"
             Title="{Binding Title}">
    
    <ContentPage.BindingContext>
        <vm:CalcDividendRateViewModel />
    </ContentPage.BindingContext>
    
    <ContentPage.Content>
        <Label Text="作成中です。"/>
    </ContentPage.Content>
</ContentPage>

CalcDividendRatePage.xaml.cs

コードビハインドです。

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace AssetManagementXamarin.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class CalcDividendRatePage : ContentPage
    {
        public CalcDividendRatePage()
        {
            InitializeComponent();
        }
    }
}

DetailCalcPage.xaml

詳細計算タブを押したときに表示されるレイアウトです。
ここも「作成中です」と表示します。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AssetManagementXamarin.Views.DetailCalcPage"
             Title="{Binding Title}"
             xmlns:local="clr-namespace:AssetManagementXamarin.ViewModels"  
             xmlns:model="clr-namespace:AssetManagementXamarin.Models"  
             x:Name="BrowseItemsPage">
    <Label Text="作成中です。"/>
</ContentPage>

DetailCalcPage.xaml.cs

コードビハインドです。

using AssetManagementXamarin.ViewModels;
using Xamarin.Forms;

namespace AssetManagementXamarin.Views
{
    public partial class DetailCalcPage : ContentPage
    {
        public DetailCalcPage()
        {
            InitializeComponent();

            BindingContext = new DetailCalcViewModel();
        }
    }
}

Controls

BaseControl.cs

ユーザーコントロールのベースとなるクラスです。

このBaseControlを継承した全てのコントロールでOnPropertyChangedを使えるように、SetPropertyメソッドを用意しています。

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

namespace AssetManagementXamarin.Controls
{
    public class BaseControl : ContentView
    {
        protected bool SetProperty<T>(ref T backingStore,
            T value,
            [CallerMemberName] string propertyName = "",
            Action onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;

            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }
    }
}

InputControl.xaml

ユーザー入力項目の1行分をまとめたコントロール(コンテンツビュー)です。
先ほどのBaseControlを継承しています。

<?xml version="1.0" encoding="UTF-8"?>
<ctrl:BaseControl xmlns="http://xamarin.com/schemas/2014/forms"
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                  xmlns:b="clr-namespace:AssetManagementXamarin.Behaviors"
                  xmlns:ctrl="clr-namespace:AssetManagementXamarin.Controls"
                  x:Class="AssetManagementXamarin.Controls.InputControl"
                  x:Name="_inputControl">
    <ContentView.Content>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="5*"/>
                <ColumnDefinition Width="5*"/>
                <ColumnDefinition Width="30"/>
            </Grid.ColumnDefinitions>
            <Label Text="{Binding Path=ItemText, Source={x:Reference _inputControl}, Mode=OneWay}"
                   Grid.Column="0" HorizontalTextAlignment="Left" VerticalTextAlignment="Center"/>
            <Frame x:Name="frame" Grid.Column="1" Padding="5,0,0,0" Margin="0,3">
                <Frame.BindingContext>
                    <x:Reference Name="_inputControl"/>
                </Frame.BindingContext>
                <Frame.Triggers>
                    <DataTrigger TargetType="Frame" Binding="{Binding HasError, Mode=OneWay}" Value="True">
                        <Setter Property="BorderColor" Value="Red"/>
                    </DataTrigger>
                    <DataTrigger TargetType="Frame" Binding="{Binding HasError, Mode=OneWay}" Value="False">
                        <Setter Property="BorderColor" Value="SlateGray"/>
                    </DataTrigger>
                </Frame.Triggers>
                <Entry x:Name="InputArea" Text="{Binding Path=InputValue, Source={x:Reference _inputControl}, Mode=TwoWay}"
                   HorizontalTextAlignment="End" VerticalTextAlignment="Center" Margin="0,0,10,0" Keyboard="Numeric">
                    <Entry.Behaviors>
                        <b:MinimumNumericEntryBehavior MinimumValue="0"/>
                    </Entry.Behaviors>
                </Entry>
            </Frame>
            <Label Text="{x:Binding Path=UnitText, Source={x:Reference _inputControl}, Mode=OneWay}"
                   Grid.Column="2" HorizontalTextAlignment="Start" VerticalTextAlignment="Center"/>
        </Grid>
    </ContentView.Content>
</ctrl:BaseControl>

InputControl.xaml.cs

コードビハインドでは、プロパティを定義しています。

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace AssetManagementXamarin.Controls
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class InputControl : BaseControl
    {
        private string _itemText;
        public string ItemText
        {
            get { return _itemText; }
            set { SetProperty(ref _itemText, value); }
        }

        private string _unitText;
        public string UnitText
        {
            get { return _unitText; }
            set { SetProperty(ref _unitText, value); }
        }

        public bool HasError
        {
            get { return (bool)GetValue(HasErrorProperty); }
            set { SetValue(HasErrorProperty, value); }
        }

        public static readonly BindableProperty HasErrorProperty =
            BindableProperty.Create("HasError", typeof(bool), typeof(InputControl), false);

        public int InputValue
        {
            get { return (int)GetValue(InputValueProperty); }
            set { SetValue(InputValueProperty, value); }
        }

        public static readonly BindableProperty InputValueProperty =
            BindableProperty.Create("InputValue", typeof(int), typeof(InputControl), 0);

        public InputControl()
        {
            InitializeComponent();
        }
    }
}

Behaviors

PlainNumericEntryBehavior.cs

テキストボックスの入力を数値のみに限定するためのビヘイビアです。

using System;
using Xamarin.Forms;

namespace AssetManagementXamarin.Behaviors
{
    internal class PlainNumericEntryBehavior : Behavior<Entry>
    {
        protected Func<double, bool> AdditionalCheck;
        protected override void OnAttachedTo(Entry bindable)
        {
            base.OnAttachedTo(bindable);
            bindable.TextChanged += TextChanged_Handler;
        }

        protected override void OnDetachingFrom(Entry bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.TextChanged -= TextChanged_Handler;
        }

        protected virtual void TextChanged_Handler(object sender, TextChangedEventArgs e)
        {
            var entry = sender as Entry;
            if (string.IsNullOrEmpty(e.NewTextValue))
            {
                entry.Text = 0.ToString();
                return;
            }

            double val;
            if (!double.TryParse(e.NewTextValue, out val))
                entry.Text = e.OldTextValue;
            else if (!AdditionalCheck?.Invoke(val) ?? false)
                entry.Text = e.OldTextValue;
            else
                entry.Text = val.ToString();
        }
    }
}

MinimumNumericEntryBehavior.cs

指定した値以上の数値のみを入力可能とするためのビヘイビアです。

先ほどのPlainNumericEntryBehavior.csを継承しており、ここでは入力の最小値のみを定義します。
InputControlの入力を0以上の数値のみと制限するために使っています。

namespace AssetManagementXamarin.Behaviors
{
    internal class MinimumNumericEntryBehavior : PlainNumericEntryBehavior
    {
        public double MinimumValue { get; set; } = 0;

        public MinimumNumericEntryBehavior()
        {
            AdditionalCheck = ValidateMinimum;
        }

        private bool ValidateMinimum(double input)
        {
            if (input < MinimumValue)
                return false;

            return true;
        }
    }
}

ViewModels

BaseViewModel.cs

各ViewModelの基底クラスです。

画面タイトルとOnPropertyChangedを実装したSetPropertyメソッドを用意しています。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace AssetManagementXamarin.ViewModels
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        string title = string.Empty;
        public string Title
        {
            get { return title; }
            set { SetProperty(ref title, value); }
        }

        protected bool SetProperty<T>(ref T backingStore, T value,
            [CallerMemberName] string propertyName = "",
            Action onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;

            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }

        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var changed = PropertyChanged;
            if (changed == null)
                return;

            changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }
}

CalcDividendRateViewModel.cs

必要利回り計算タブを表示する画面のタイトルを定義しています。

namespace AssetManagementXamarin.ViewModels
{
    public class CalcDividendRateViewModel : BaseViewModel
    {
        public CalcDividendRateViewModel()
        {
            Title = "簡易計算";
        }
    }
}

CalcGoalYearViewModel.cs

コードが少し長いですが、ほとんどがプロパティです。
実際に行っていることは、計算ボタンをクリックした時の入力値チェックと、シミュレーション結果の表示です。

using AssetManagementXamarin.Models;
using Microsoft.Toolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Windows.Input;

namespace AssetManagementXamarin.ViewModels
{
    public class CalcGoalYearViewModel : BaseViewModel
    {
        private static int _thisYear = DateTime.Now.Year;

        private ICommand _buttonClickCommand;
        public ICommand ButtonClickCommand => _buttonClickCommand ?? (_buttonClickCommand = new RelayCommand(OnButtonClick));

        #region props

        private IEnumerable<ResultModel> _gridData = new List<ResultModel>();
        public IEnumerable<ResultModel> GridData
        {
            get { return _gridData; }
            set { SetProperty(ref _gridData, value); }
        }

        private int _startYear = _thisYear;
        public int StartYear
        {
            get { return _startYear; }
            set { SetProperty(ref _startYear, value); }
        }

        private int _startAsset;
        public int StartAsset
        {
            get { return _startAsset; }
            set { SetProperty(ref _startAsset, value); }
        }

        private int _goalDividend;
        public int GoalDividend
        {
            get { return _goalDividend; }
            set { SetProperty(ref _goalDividend, value); }
        }

        private int _expectedDividendRate;
        public int ExpectedDividendRate
        {
            get { return _expectedDividendRate; }
            set { SetProperty(ref _expectedDividendRate, value); }
        }

        private int _anualAffordableCash;
        public int AnualAffordableCash
        {
            get { return _anualAffordableCash; }
            set { SetProperty(ref _anualAffordableCash, value); }
        }

        private bool _isCompound;
        public bool IsCompound
        {
            get { return _isCompound; }
            set { SetProperty(ref _isCompound, value); }
        }

        private bool _hasErrorGoalDividend;
        public bool HasErrorGoalDividend
        {
            get { return _hasErrorGoalDividend; }
            set { SetProperty(ref _hasErrorGoalDividend, value); }
        }

        private bool _hasErrorExpectedDividendRate;
        public bool HasErrorExpectedDividendRate
        {
            get { return _hasErrorExpectedDividendRate; }
            set { SetProperty(ref _hasErrorExpectedDividendRate, value); }
        }

        private bool _hasErrorAnualAffordableCash;
        public bool HasErrorAnualAffordableCash
        {
            get { return _hasErrorAnualAffordableCash; }
            set { SetProperty(ref _hasErrorAnualAffordableCash, value); }
        }

        private bool _hasError;
        public bool HasError
        {
            get { return _hasError; }
            set { SetProperty(ref _hasError, value); }
        }

        private int _resultValue;
        public int ResultValue
        {
            get { return _resultValue; }
            set { SetProperty(ref _resultValue, value); }
        }

        private bool _resultVisibility;
        public bool ResultVisibility
        {
            get { return _resultVisibility; }
            set { SetProperty(ref _resultVisibility, value); }
        }

        #endregion

        public CalcGoalYearViewModel()
        {
            Title = "簡易計算";
        }

        private void OnButtonClick()
        {
            HasError = !Validate();
            if (HasError)
            {
                ResultValue = 0;
                ResultVisibility = false;
                GridData = null;
                return;
            }

            CreateGridData();
            ResultVisibility = true;
        }

        private bool Validate()
        {
            HasErrorGoalDividend = GoalDividend <= 0;
            HasErrorExpectedDividendRate = ExpectedDividendRate <= 0;
            return !HasErrorGoalDividend && !HasErrorExpectedDividendRate;
        }

        private void CreateGridData()
        {
            int currentAsset = StartAsset;
            var data = new List<ResultModel>();

            var counter = 0;
            while(true)
            {
                var year = StartYear + counter;
                currentAsset += AnualAffordableCash;
                var expectedDividend = currentAsset * ExpectedDividendRate / 100;

                data.Add(new ResultModel(year, currentAsset, expectedDividend));

                if (expectedDividend > GoalDividend)
                {
                    GridData = data;
                    ResultValue = year;
                    return;
                }

                if (IsCompound)
                    currentAsset += expectedDividend;

                counter++;
            }
        }
    }
}

CalcRequiredMoneyViewModel.cs

こちらもCalcGoalYearViewModel.csと同じくほとんどがプロパティです。
CalcGoalYearViewModel.csと比べると、CreateGridDataの中で行うシミュレーション結果の計算が異なります。

using AssetManagementXamarin.Models;
using Microsoft.Toolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Windows.Input;

namespace AssetManagementXamarin.ViewModels
{
    public class CalcRequiredMoneyViewModel : BaseViewModel
    {
        private static int _thisYear = DateTime.Now.Year;

        private ICommand _buttonClickCommand;
        public ICommand ButtonClickCommand => _buttonClickCommand ?? (_buttonClickCommand = new RelayCommand(OnButtonClick));

        #region props

        private IEnumerable<ResultModel> _gridData = new List<ResultModel>();
        public IEnumerable<ResultModel> GridData
        {
            get { return _gridData; }
            set { SetProperty(ref _gridData, value); }
        }

        private int _startYear = _thisYear;
        public int StartYear
        {
            get { return _startYear; }
            set { SetProperty(ref _startYear, value); }
        }

        private int _startAsset;
        public int StartAsset
        {
            get { return _startAsset; }
            set { SetProperty(ref _startAsset, value); }
        }

        private int _goalDividend;
        public int GoalDividend
        {
            get { return _goalDividend; }
            set { SetProperty(ref _goalDividend, value); }
        }

        private int _goalRetireYear = _thisYear;
        public int GoalRetireYear
        {
            get { return _goalRetireYear; }
            set { SetProperty(ref _goalRetireYear, value); }
        }

        private int _expectedDividendRate;
        public int ExpectedDividendRate
        {
            get { return _expectedDividendRate; }
            set { SetProperty(ref _expectedDividendRate, value); }
        }

        private bool _isCompound;
        public bool IsCompound
        {
            get { return _isCompound; }
            set { SetProperty(ref _isCompound, value); }
        }

        private bool _hasErrorGoalRetireYear;
        public bool HasErrorGoalRetireYear
        {
            get { return _hasErrorGoalRetireYear; }
            set { SetProperty(ref _hasErrorGoalRetireYear, value); }
        }

        private bool _hasErrorGoalDividend;
        public bool HasErrorGoalDividend
        {
            get { return _hasErrorGoalDividend; }
            set { SetProperty(ref _hasErrorGoalDividend, value); }
        }

        private bool _hasErrorExpectedDividendRate;
        public bool HasErrorExpectedDividendRate
        {
            get { return _hasErrorExpectedDividendRate; }
            set { SetProperty(ref _hasErrorExpectedDividendRate, value); }
        }

        private bool _hasError;
        public bool HasError
        {
            get { return _hasError; }
            set { SetProperty(ref _hasError, value); }
        }

        private int _resultValue;
        public int ResultValue
        {
            get { return _resultValue; }
            set { SetProperty(ref _resultValue, value); }
        }

        private bool _resultVisibility;
        public bool ResultVisibility
        {
            get { return _resultVisibility; }
            set { SetProperty(ref _resultVisibility, value); }
        }

        #endregion

        public CalcRequiredMoneyViewModel()
        {
            Title = "簡易計算";
        }

        private void OnButtonClick()
        {
            var timePeriod = GoalRetireYear - StartYear + 1;

            HasError = !Validate(timePeriod);
            if (HasError)
            {
                ResultValue = 0;
                ResultVisibility = false;
                GridData = null;
                return;
            }

            var requiredAmount = GoalDividend * 100 / ExpectedDividendRate - StartAsset;
            ResultValue = requiredAmount <= 0 ? 0 : requiredAmount / timePeriod;
            CreateGridData(timePeriod);
            ResultVisibility = true;
        }

        private bool Validate(int timePeriod)
        {
            HasErrorGoalRetireYear = timePeriod <= 0;
            HasErrorGoalDividend = GoalDividend <= 0;
            HasErrorExpectedDividendRate = ExpectedDividendRate <= 0;
            return !HasErrorGoalRetireYear && !HasErrorGoalDividend && !HasErrorExpectedDividendRate;
        }

        private void CreateGridData(int timePeriod)
        {
            int currentAsset = StartAsset;
            var data = new List<ResultModel>();
            for (int i = 0; i < timePeriod; i++)
            {
                var year = StartYear + i;
                currentAsset += ResultValue;

               data.Add(new ResultModel(year, currentAsset, currentAsset * ExpectedDividendRate / 100));
            }
            GridData = data;
        }
    }
}

DetailCalcViewModel.cs

詳細計算タブを表示する画面のタイトルを定義しています。

namespace AssetManagementXamarin.ViewModels
{
    public class DetailCalcViewModel : BaseViewModel
    {
        public DetailCalcViewModel()
        {
            Title = "詳細計算";
        }
    }
}

Models

ResultModel.cs

ユーザー入力を保持するクラスです。

namespace AssetManagementXamarin.Models
{
    public class ResultModel
    {
        public int Year { get; set; }
        public int TotalAsset { get; }
        public int Dividend { get; }

        public ResultModel(int year, int totalAsset, int dividend)
        {
            Year = year;
            TotalAsset = totalAsset;
            Dividend = dividend;
        }
    }
}

最後に & 関連書籍

サンプルとして作成したので未実装の個所がありますが、以上になります。

分かりにくいところや「もっとこうしてほしい」などのご意見がありましたら、ツイッターから連絡いただけると幸いです。
質問に関してもお気軽にお問い合わせください。

以下は関連書籍です。

コメント

タイトルとURLをコピーしました