UWP 資産管理システム サンプルコード

業務でUWPの開発に携わることになったのですが、UWPで一番困るのは日本語の解説が少ないことです。
サンプルコードもあまりなく、とっかかるのが難しいです。
そこで、勉強用に作成したサンプルコードを公開します。
業務でUWPに携わることになった方の役に立てば幸いです。

以下の記事で、このUWPで作成したシステムをAndroidとiOSのアプリに置き換えました。
スマホアプリの開発に興味がある方は見てみてください。

完成画面

まず完成画面です。
簡易計算タブと詳細計算タブがあり、簡易計算タブの中に必要積立金計算タブと必要利回り計算タブがある形となっています。
サンプルコードのため、詳細計算タブと必要利回り計算タブの中身は「作成中です」とのみ表示しています。

UWP-資産管理システム-完成画面

フォルダ構成

フォルダ構成は次のようになります。

UWP-資産管理システム-フォルダ構成

では、それぞれのファイルを説明していきます。

Views

MainPage.xaml

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

残念ながら現時点ではScrollViewerにバグがあります。
そのため、ScrollViewerのIsTabStop=”True”としています。
バグの詳細について気になる方はhttps://github.com/microsoft/microsoft-ui-xaml/issues/597を参照してみてください。

<Page
    x:Class="AssetManagementUWP.Views.MainPage"
    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:helpers="using:AssetManagementUWP.Helpers"
    xmlns:winui="using:Microsoft.UI.Xaml.Controls"
    xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    xmlns:views="using:AssetManagementUWP.Views"
    Style="{StaticResource PageStyle}"
    mc:Ignorable="d"
    MinHeight="685">
    
    <Grid x:Name="ContentArea">
        <winui:NavigationView x:Name="navigationView" IsBackButtonVisible="Collapsed"
                              IsSettingsVisible="False" PaneDisplayMode="Top"
                              Background="{ThemeResource SystemControlBackgroundAltHighBrush}">
            <winui:NavigationView.MenuItems>
                <winui:NavigationViewItem x:Uid="SimpleCalc" Content="簡易計算" helpers:NavHelper.NavigateTo="views:SimpleCalcPage"/>
                <winui:NavigationViewItem x:Uid="DetailCalc" Content="詳細計算" helpers:NavHelper.NavigateTo="views:DetailCalcPage"/>
            </winui:NavigationView.MenuItems>
            <i:Interaction.Behaviors>
                <ic:EventTriggerBehavior EventName="ItemInvoked">
                    <ic:InvokeCommandAction Command="{x:Bind ViewModel.ItemInvokedCommand}"/>
                </ic:EventTriggerBehavior>
            </i:Interaction.Behaviors>
            <Grid>
                <!--ScrollViewerのFocusが移動してしまうバグがあるため、IsTabStop="True"とし、UseSystemFocusVisuals="False"にする-->
                <!--参考:https://github.com/microsoft/microsoft-ui-xaml/issues/597-->
                <ScrollViewer x:Name="scrollViewer" IsTabStop="True" UseSystemFocusVisuals="False">
                    <Frame x:Name="mainFrame" Margin="24,10,24,15" MaxWidth="800"/>
                </ScrollViewer>
            </Grid>
        </winui:NavigationView>
    </Grid>
</Page>

MainPage.xaml.cs

MVVMパターンで作成する場合、基本的にコードビハインドには何も書きません。
ここでは起動画面のサイズの設定のみ行っています。

using AssetManagementUWP.ViewModels;
using Windows.Foundation;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml.Controls;

namespace AssetManagementUWP.Views
{
    internal sealed partial class MainPage : Page
    {
        public MainViewModel ViewModel { get; } = new MainViewModel();

        public MainPage()
        {
            this.InitializeComponent();
            ApplicationView.PreferredLaunchViewSize = new Size(600, 685);
            ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;
            ViewModel.Initialize(navigationView, mainFrame);
        }
    }
}

SimpleCalcPage.xaml

簡易計算タブの中身です。

ユーザー入力を受け付けるコントロールは、 入力単位1行分をInputControlとしてまとめています。
以下の1行がInputControlになります。

UWP-資産管理システム-InputControl

InputControl に関しては、後で説明します。

簡易計算タブの中で「必要積立金計算」と「必要利回り計算」がタブで切り替わるようになっています。
これはPivotというコントロールで実現しています。

<Page
    x:Class="AssetManagementUWP.Views.SimpleCalcPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="using:AssetManagementUWP.Controls"
    mc:Ignorable="d"
    Background="White">
    <Grid>
        <StackPanel>
            <StackPanel.Resources>
                <Style TargetType="local:InputControl">
                    <Setter Property="Margin" Value="25,15,25,15"/>
                </Style>
                <Style TargetType="Rectangle">
                    <Setter Property="HorizontalAlignment" Value="Stretch"/>
                    <Setter Property="Fill" Value="Gray"/>
                    <Setter Property="Height" Value="0.9"/>
                </Style>
                <Style TargetType="StackPanel">
                    <Setter Property="BorderBrush" Value="Gray"/>
                    <Setter Property="Orientation" Value="Vertical"/>
                    <Setter Property="HorizontalAlignment" Value="Stretch"/>
                    <Setter Property="VerticalAlignment" Value="Stretch"/>
                </Style>
            </StackPanel.Resources>
            <TextBlock Text="現状と目標" HorizontalAlignment="Center" Style="{StaticResource SubtitleTextBlockStyle}"/>
            <StackPanel BorderThickness="1">
                <local:InputControl x:Name="InputStartYearControl" ItemText="投資を開始した年" UnitText="年" Format="Year" SpinButtonPlacementMode="Compact"
                                    InputValue="{x:Bind ViewModel.StartYear, Mode=TwoWay}" HasError="{x:Bind ViewModel.HasErrorGoalRetireYear, Mode=OneWay}"/>
                <Rectangle/>
                <local:InputControl x:Name="InputStartAssetControl" ItemText="投資開始時の資産" UnitText="万円" Format="Currency"
                                    InputValue="{x:Bind ViewModel.StartAsset, Mode=TwoWay}"/>
                <Rectangle/>
                <local:InputControl x:Name="InputGoalDividendControl" ItemText="1年間で欲しい配当額" UnitText="万円" Format="Currency"
                                    InputValue="{x:Bind ViewModel.GoalDividend, Mode=TwoWay}" HasError="{x:Bind ViewModel.HasErrorGoalDividend, Mode=OneWay}"/>
                <Rectangle/>
                <local:InputControl x:Name="InputGoalRetireYearControl" ItemText="FIRE達成したい年" UnitText="年" Format="Year" SpinButtonPlacementMode="Compact"
                                    InputValue="{x:Bind ViewModel.GoalRetireYear, Mode=TwoWay}" HasError="{x:Bind ViewModel.HasErrorGoalRetireYear, Mode=OneWay}"/>
            </StackPanel>
            <Pivot x:Name="Tabs" Margin="0,5,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Stretch">
                <Pivot.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Style="{ThemeResource BodyTextBlockStyle}" Text="{Binding}"/>
                    </DataTemplate>
                </Pivot.HeaderTemplate>
                <PivotItem Header="必要積立金計算">
                    <StackPanel>
                        <StackPanel BorderThickness="1">
                            <local:InputControl x:Name="InputExpectedDividendRateControl" ItemText="想定年利" UnitText="%" Format="Percent"
                                            InputValue="{x:Bind ViewModel.ExpectedDividendRate, Mode=TwoWay}" HasError="{x:Bind ViewModel.HasErrorExpectedDividendRate, Mode=OneWay}"/>
                            <Rectangle/>
                            <Button Name="CalcButton" Content="計算" Command="{x:Bind ViewModel.ButtonClickCommand}"
                                VerticalAlignment="Center" HorizontalAlignment="Center"
                                Width="160" Height="50" Margin="0,15,0,15" Background="LightBlue"/>
                        </StackPanel>
                        <StackPanel x:Name="resultPanel" Visibility="{x:Bind ViewModel.ResultVisibility, Mode=OneWay}">
                            <TextBlock Text="シミュレーション結果" Style="{StaticResource SubtitleTextBlockStyle}" HorizontalAlignment="Center" Margin="0,20,0,5"/>
                            <StackPanel BorderThickness="1">
                                <TextBlock Text="必要年間積み立て額" Style="{StaticResource BodyTextBlockStyle}" HorizontalAlignment="Center" Margin="0,10,0,3"/>
                                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                                    <StackPanel.Resources>
                                        <Style TargetType="TextBlock" BasedOn="{StaticResource SubtitleTextBlockStyle}">
                                            <Setter Property="FontWeight" Value="ExtraBold"/>
                                            <Setter Property="HorizontalAlignment" Value="Center"/>
                                        </Style>
                                    </StackPanel.Resources>
                                    <TextBlock Text="{x:Bind ViewModel.ResultValue, Mode=OneWay}"/>
                                    <TextBlock Text="万円"/>
                                </StackPanel>
                                <controls:DataGrid x:Name="ResultGrid" ItemsSource="{x:Bind ViewModel.GridData}" 
                                               AutoGenerateColumns="False" GridLinesVisibility="Horizontal" Margin="20,20,20,20"
                                               HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
                                    <controls:DataGrid.Columns>
                                        <controls:DataGridTextColumn Binding="{Binding Year}" Header="年"/>
                                        <controls:DataGridTextColumn Binding="{Binding TotalAsset}" Header="資産額(万円)"/>
                                        <controls:DataGridTextColumn Binding="{Binding Dividend}" Header="年間配当(万円)"/>
                                    </controls:DataGrid.Columns>
                                </controls:DataGrid>
                            </StackPanel>
                        </StackPanel>
                    </StackPanel>
                </PivotItem>
                <PivotItem Header="必要利回り計算">
                    <StackPanel BorderBrush="Black" BorderThickness="2">
                        <TextBlock Text="TODO 作成中です。"/>
                    </StackPanel>
                </PivotItem>
            </Pivot>
            </StackPanel>
    </Grid>
</Page>

SimpleCalcPage.xaml.cs

コードビハインドでは、ViewModelの指定のみ行います。

using AssetManagementUWP.ViewModels;
using Windows.UI.Xaml.Controls;

namespace AssetManagementUWP.Views
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    internal sealed partial class SimpleCalcPage : Page
    {
        public SimpleCalcViewModel ViewModel { get; } = new SimpleCalcViewModel();

        public SimpleCalcPage()
        {
            this.InitializeComponent();
        }
    }
}

DetailCalcPage.xaml

簡易計算タブの中身です。
「作成中です」とのみ表示します。

<Page
    x:Class="AssetManagementUWP.Views.DetailCalcPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AssetManagementUWP.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <TextBlock Text="作成中です。"/>
    </Grid>
</Page>

DetailCalcPage.xaml.cs

コードビハインドです。

using Windows.UI.Xaml.Controls;

namespace AssetManagementUWP.Views
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class DetailCalcPage : Page
    {
        public DetailCalcPage()
        {
            this.InitializeComponent();
        }
    }
}

Styles

Page.xaml

小さなシステムなので作らなくても管理できますが、参考のためStyleを1つ作成しました。
大きなシステムでは、Styleを別ファイルに切り出しておくと管理がしやすいです。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style TargetType="Page" x:Key="PageStyle">
        <Setter Property="Background" Value="{ThemeResource ApplicationPageBackgroundThemeBrush}"/>
    </Style>
</ResourceDictionary>

Controls

InputControl.xaml

SimpleCalcPage.xamlで紹介したユーザーコントロールです。
重複するコード量を減らすため、ユーザー入力1行分をテンプレートとしてまとめています。

<UserControl
    x:Class="AssetManagementUWP.Controls.InputControl"
    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:winui="using:Microsoft.UI.Xaml.Controls"
    xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    mc:Ignorable="d"
    d:DesignHeight="100"
    d:DesignWidth="100">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="6*"/>
            <ColumnDefinition Width="4*"/>
            <ColumnDefinition Width="27"/>
        </Grid.ColumnDefinitions>
        <TextBlock Text="{x:Bind Path=ItemText}" Grid.Column="0"
                   HorizontalAlignment="Left" VerticalAlignment="Center"/>
        <winui:NumberBox x:Name="InputArea" Value="{x:Bind InputValue, Mode=TwoWay}" Grid.Column="1"
                         HorizontalContentAlignment="Right" HorizontalAlignment="Right" VerticalAlignment="Center"  VerticalContentAlignment="Center" 
                         Margin="0,0,10,0" ValidationMode="InvalidInputOverwritten" SpinButtonPlacementMode="{x:Bind SpinButtonPlacementMode}" Minimum="0" BorderThickness="0.4">
            <i:Interaction.Behaviors>
                <ic:DataTriggerBehavior Binding="{x:Bind HasError, Mode=TwoWay}" Value="True">
                    <ic:ChangePropertyAction TargetObject="{x:Bind InputArea}" PropertyName="BorderBrush" Value="Red"/>
                </ic:DataTriggerBehavior>
                <ic:DataTriggerBehavior Binding="{x:Bind HasError, Mode=TwoWay}" Value="False">
                    <ic:ChangePropertyAction TargetObject="{x:Bind InputArea}" PropertyName="BorderBrush" Value="Gray" />
                </ic:DataTriggerBehavior>
            </i:Interaction.Behaviors>
        </winui:NumberBox>
        <TextBlock Text="{x:Bind Path=UnitText}" Grid.Column="2"
                   HorizontalAlignment="Right" VerticalAlignment="Center"/>
    </Grid>
</UserControl>

InputControl.xaml.cs

表示文言の設定や、入力欄の右側に表示するスピンボタンの有無を設定したりしています。

using Microsoft.UI.Xaml.Controls;
using System;
using Windows.Globalization.NumberFormatting;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace AssetManagementUWP.Controls
{
    public sealed partial class InputControl : UserControl
    {
        public string ItemText { get; set; }
        public string UnitText { get; set; }

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

        // Using a DependencyProperty as the backing store for HasErrorProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HasErrorProperty =
            DependencyProperty.Register("HasError", typeof(bool), typeof(InputControl), new PropertyMetadata(false));
        public int InputValue
        {
            get { return (int)GetValue(InputValueProperty); }
            set { SetValue(InputValueProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InputValue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InputValueProperty =
            DependencyProperty.Register("InputValue", typeof(int), typeof(InputControl), new PropertyMetadata(0));


        public  NumberBoxFormat Format
        {
            get { return ( NumberBoxFormat)GetValue(FormatProperty); }
            set { SetValue(FormatProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Format.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty FormatProperty =
            DependencyProperty.Register("Format", typeof( NumberBoxFormat), typeof(InputControl), new PropertyMetadata(NumberBoxFormat.Year, new PropertyChangedCallback(SetFormat)));



        public NumberBoxSpinButtonPlacementMode SpinButtonPlacementMode
        {
            get { return (NumberBoxSpinButtonPlacementMode)GetValue(SpinButtonPlacementModeProperty); }
            set { SetValue(SpinButtonPlacementModeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SpinButtonPlacementMode.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SpinButtonPlacementModeProperty =
            DependencyProperty.Register("SpinButtonPlacementMode", typeof(NumberBoxSpinButtonPlacementMode), typeof(InputControl), new PropertyMetadata(NumberBoxSpinButtonPlacementMode.Hidden));



        #endregion

        public InputControl()
        {
            this.InitializeComponent();
            (this.Content as FrameworkElement).DataContext = this;
        }

        private static void SetFormat(DependencyObject dp, DependencyPropertyChangedEventArgs args)
        {
            InputControl control = dp as InputControl;
            switch (control.Format)
            {
                case NumberBoxFormat.Percent:
                    //TODO 利回りは小数第二桁まで入力可能にする
                    //control.InputArea.NumberFormatter = CreateDecimalFormatter();
                    control.InputArea.NumberFormatter = CreateIntFormatter();
                    break;
                case NumberBoxFormat.Year:
                    control.InputArea.NumberFormatter = CreateIntFormatter();
                    break;
                case NumberBoxFormat.Currency:
                    control.InputArea.NumberFormatter = CreateCurrencyFormatter();
                    break;
                default: 
                    throw new ArgumentOutOfRangeException(nameof(Format));
            }
        }

        private static INumberFormatter2 CreateDecimalFormatter()
        {
            var rounder = new IncrementNumberRounder();
            rounder.Increment = 0.01;
            rounder.RoundingAlgorithm = RoundingAlgorithm.RoundHalfUp;

            var formatter = new DecimalFormatter();
            formatter.FractionDigits = 2;
            formatter.NumberRounder = rounder;
            return formatter;
        }

        private static INumberFormatter2 CreateIntFormatter()
        {
            var formatter = new DecimalFormatter();
            formatter.FractionDigits = 0;
            return formatter;
        }

        private static INumberFormatter2 CreateCurrencyFormatter()
        {
            //var formatter = new CurrencyFormatter(CurrencyIdentifiers.JPY);
            var formatter = new DecimalFormatter();
            formatter.FractionDigits =  0;
            return formatter;
        }

        public enum NumberBoxFormat
        {
            Percent,
            Year,
            Currency
        }
    }
}

ViewModels

MainViewModel.cs

簡易計算タブと詳細計算タブが切り替わった時、MainViewModelのOnItemInvokedが実行されます。
そこで画面遷移先の型 (Type) を取得し、NavigationServiceで画面を遷移します。
NavigationServiceについては後で説明します。

Initializeでは、初期表示で簡易計算タブの中身が表示されるようにしています。

using AssetManagementUWP.Helpers;
using AssetManagementUWP.Services;
using AssetManagementUWP.Views;
using Microsoft.Toolkit.Mvvm.Input;
using System;
using System.Linq;
using System.Windows.Input;
using Windows.UI.Xaml.Controls;
using WinUI = Microsoft.UI.Xaml.Controls;

namespace AssetManagementUWP.ViewModels
{
    internal class MainViewModel
    {
        private WinUI.NavigationView _navigationView;
        private ICommand _itemInvokedCommand;

        public ICommand ItemInvokedCommand => _itemInvokedCommand ?? (_itemInvokedCommand = new RelayCommand<WinUI.NavigationViewItemInvokedEventArgs>(OnItemInvoked));

        private void OnItemInvoked(WinUI.NavigationViewItemInvokedEventArgs args)
        {
            var selectedItem = args.InvokedItemContainer as WinUI.NavigationViewItem;
            var pageType = selectedItem?.GetValue(NavHelper.NavigateToProperty) as Type;
            NavigationService.Navigate(pageType, null, args.RecommendedNavigationTransitionInfo);
        }

        public void Initialize(WinUI.NavigationView navigationView, Frame frame)
        {
            _navigationView = navigationView;
            NavigationService.Frame = frame;
            ////TODO 以下暫定処理。システムが大きくなる時にActivationServiceを実装したら、ここはdefaultでSimpleCalcPageが取れるようにする
            NavigationService.Navigate(typeof(SimpleCalcPage));
            _navigationView.SelectedItem = _navigationView.MenuItems.First();
        }
    }
}

SimpleCalcViewModel.cs

簡易計算タブのViewModelです。

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

using AssetManagementUWP.Models;
using Microsoft.Toolkit.Mvvm.Input;
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Windows.UI.Xaml;

namespace AssetManagementUWP.ViewModels
{
    internal class SimpleCalcViewModel : DependencyObject
    {
        private static int _thisYear = DateTime.Now.Year;

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

        public ObservableCollection<ResultModel> GridData { get; } = new ObservableCollection<ResultModel>();

        #region dp

        public int StartYear
        {
            get { return (int)GetValue(StartYearProperty); }
            set { SetValue(StartYearProperty, value); }
        }

        // Using a DependencyProperty as the backing store for StartYear.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty StartYearProperty =
            DependencyProperty.Register("StartYear", typeof(int), typeof(SimpleCalcViewModel), new PropertyMetadata(_thisYear));

        public int StartAsset
        {
            get { return (int)GetValue(StartAssetProperty); }
            set { SetValue(StartAssetProperty, value); }
        }

        // Using a DependencyProperty as the backing store for StartAsset.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty StartAssetProperty =
            DependencyProperty.Register("StartAsset", typeof(int), typeof(SimpleCalcViewModel), new PropertyMetadata(0));

        public int GoalDividend
        {
            get { return (int)GetValue(GoalDividendProperty); }
            set { SetValue(GoalDividendProperty, value); }
        }

        // Using a DependencyProperty as the backing store for GoalDividend.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GoalDividendProperty =
            DependencyProperty.Register("GoalDividend", typeof(int), typeof(SimpleCalcViewModel), new PropertyMetadata(0));

        public int GoalRetireYear
        {
            get { return (int)GetValue(GoalRetireYearProperty); }
            set { SetValue(GoalRetireYearProperty, value); }
        }

        // Using a DependencyProperty as the backing store for GoalRetireYear.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GoalRetireYearProperty =
            DependencyProperty.Register("GoalRetireYear", typeof(int), typeof(SimpleCalcViewModel), new PropertyMetadata(_thisYear));

        public int ExpectedDividendRate
        {
            get { return (int)GetValue(ExpectedDividendRateProperty); }
            set { SetValue(ExpectedDividendRateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ExpectedDividendRate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ExpectedDividendRateProperty =
            DependencyProperty.Register("ExpectedDividendRate", typeof(int), typeof(SimpleCalcViewModel), new PropertyMetadata(0));

        public bool HasErrorGoalRetireYear
        {
            get { return (bool)GetValue(HasErrorGoalRetireYearProperty); }
            set { SetValue(HasErrorGoalRetireYearProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HasErrorGoalRetireYear.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HasErrorGoalRetireYearProperty =
            DependencyProperty.Register("HasErrorGoalRetireYear", typeof(bool), typeof(SimpleCalcViewModel), new PropertyMetadata(false));

        public bool HasErrorGoalDividend
        {
            get { return (bool)GetValue(HasErrorGoalDividendProperty); }
            set { SetValue(HasErrorGoalDividendProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HasErrorGoalDividend.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HasErrorGoalDividendProperty =
            DependencyProperty.Register("HasErrorGoalDividend", typeof(bool), typeof(SimpleCalcViewModel), new PropertyMetadata(false));

        public bool HasErrorExpectedDividendRate
        {
            get { return (bool)GetValue(HasErrorExpectedDividendRateProperty); }
            set { SetValue(HasErrorExpectedDividendRateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HasErrorExpectedDividendRate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HasErrorExpectedDividendRateProperty =
            DependencyProperty.Register("HasErrorExpectedDividendRate", typeof(bool), typeof(SimpleCalcViewModel), new PropertyMetadata(false));

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

        // Using a DependencyProperty as the backing store for HasError.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HasErrorProperty =
            DependencyProperty.Register("HasError", typeof(bool), typeof(SimpleCalcViewModel), new PropertyMetadata(false));

        public int ResultValue
        {
            get { return (int)GetValue(ResultValueProperty); }
            set { SetValue(ResultValueProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ResultValue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ResultValueProperty =
            DependencyProperty.Register("ResultValue", typeof(int), typeof(SimpleCalcViewModel), new PropertyMetadata(0));

        public Visibility ResultVisibility
        {
            get { return (Visibility)GetValue(ResultVisibilityProperty); }
            set { SetValue(ResultVisibilityProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ResultVisibility.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ResultVisibilityProperty =
            DependencyProperty.Register("ResultVisibility", typeof(Visibility), typeof(SimpleCalcViewModel), new PropertyMetadata(Visibility.Collapsed));

        #endregion

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

            HasError = !Validate(timePeriod);
            if (HasError)
            {
                ResultValue = 0;
                ResultVisibility = Visibility.Collapsed;
                return;
            }

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

        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;
            for(int i = 0; i < timePeriod; i++)
            {
                var year = StartYear + i;
                currentAsset += ResultValue;

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

Services

画面遷移を行うサービスです。
スパゲティコードにならないよう、基本的には1クラスにつき1つの役割です。
ここでは画面遷移のみ実装しています。

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;

namespace AssetManagementUWP.Services
{
    internal static class NavigationService
    {
        private static Frame _frame;
        public static Frame Frame
        {
            get
            {
                if (_frame == null)
                {
                    _frame = Window.Current.Content as Frame;
                }
                return _frame;
            }
            set
            {
                _frame = value;
            }
        } 

        public static void Navigate(Type pageType, object parameter = null, NavigationTransitionInfo info = null)
        {
            if (Frame.Content?.GetType() == pageType) return;

            Frame.Navigate(pageType, parameter, info);
        }
    }
}

Helpers

画面遷移のヘルパークラスになります。

MainViewModel.csのところで説明しましたが、簡易計算タブと詳細計算タブが切り替わった時、MainViewModelのOnItemInvokedが実行されます。
4行目で画面遷移先の型 (Type) を取得しています。
5行目では、4行目で取得した型を指定することで、遷移するページを指定します。

        private void OnItemInvoked(WinUI.NavigationViewItemInvokedEventArgs args)
        {
            var selectedItem = args.InvokedItemContainer as WinUI.NavigationViewItem;
            var pageType = selectedItem?.GetValue(NavHelper.NavigateToProperty) as Type;
            NavigationService.Navigate(pageType, null, args.RecommendedNavigationTransitionInfo);
        }

NavHelper.csのコードは以下になります。
NavigateToという添付プロパティを実装しています。

using System;
using Windows.UI.Xaml;

namespace AssetManagementUWP.Helpers
{
    public class NavHelper
    {
        public static Type GetNavigateTo(DependencyObject obj)
        {
            return (Type)obj.GetValue(NavigateToProperty);
        }

        public static void SetNavigateTo(DependencyObject obj, Type value)
        {
            obj.SetValue(NavigateToProperty, value);
        }

        // Using a DependencyProperty as the backing store for NavigateTo.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty NavigateToProperty =
            DependencyProperty.RegisterAttached("NavigateTo", typeof(Type), typeof(NavHelper), new PropertyMetadata(null));
    }
}

Models

ResultModel.cs

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

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

namespace AssetManagementUWP.Models
{
    internal class ResultModel
    {
        public int Year { get; }
        public int TotalAsset { get; }
        public int Dividend { get; }

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

最後に & 関連書籍

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

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

以下は関連書籍です。

コメント

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