びおりんのブログ

きままにアプリのできるまでを公開していきます

.NETCore用Prismテンプレートの構成確認(2)

久々に更新

.NETCore用Prismテンプレートである
Prism Full App(.NET Core)プロジェクトテンプレートの構成を
前回の記事で大雑把に捉えました

今回から構成を細かくひも解いてコードと動作の関連を見ていきます

第1回は、.NETCore用PrismテンプレートにおいてMainWindow.xaml
パーツとして同一モジュールのViewを表示する機能を一から実装します

具体的には、prismを使ったWPFではRegionという機能を使って
文字列を合言葉にして自前のViewやButtonといったコントロール
呼び出して生成し、画面に表示させます

下準備

.NETCore用Prismテンプレートのうち中身がプレーンな
Prism Blank App.(.NET Core)プロジェクトテンプレートを
Unityコンテナ付きで作成します
f:id:kazoojapan1985:20200628203206p:plain

MainWindow.xamlには既にパーツを表示するためのスペースが
ContentControlに"ContentRegion"という名前で作成されています

<Window x:Class="BlankCoreApp1.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="{Binding Title}" Height="350" Width="525" >
    <Grid>
        <ContentControl prism:RegionManager.RegionName="ContentRegion" />
    </Grid>
</Window>

次に上記スペースに表示させるためのパーツとしてContentViewを作成します

ソリューションエクスプローラー -> Viewsフォルダ -> コンテキストメニュー
-> 追加 -> 新しい項目 -> ユーザーコントロール -> 名前 -> ContentView.xaml -> 追加

作成したContentView.xamlは無地なので、適当なボタンを設置して見えるようにしておきます

<UserControl x:Class="BlankCoreApp1.Views.ContentView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:BlankCoreApp1.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Button Background="AliceBlue" Width="50" Height="50" FontSize="30" Content="1"/>
    </Grid>
</UserControl>

ContentRegion文字列からContentViewを呼び出す

これからContentRegionという文字列を合言葉としてContentViewを
RegionManagerという機能を利用して呼び出します

RegionManagerを利用するためには下記が必要です
1. コンテナに呼び出したいコントロールが登録されている
2. 合言葉用文字列とコントロールが紐づけされている

コンテナに呼び出したいコントロールを登録する

App.xaml.csにて、RegisterTypesメソッドに下記を追記します

   /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App
    {
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterForNavigation<ContentView>();
        }
    }

文字列とコントロールを紐づけする

呼び出し元のMainWindow.xamlと呼び出し先のContentView.xamlが同一プロジェクトに存在する場合
呼び出し元に対応するMainWIndowModel.csにて、コンストラクタ引数にIRegionManagerを設定し
"ContentRegion"文字列とContentViewコントロールを紐づけます

   public class MainWindowViewModel : BindableBase
    {
        private string _title = "Prism Application";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        public MainWindowViewModel(IRegionManager regionManager)
        {
            regionManager.RegisterViewWithRegion("ContentRegion", typeof(ContentView));
        }
    }

実行結果

起動すると下記手順でMainWindow.xaml上にContentView.xaml(数字付きボタン)が表示されます
1. App.RegisterTypesメソッドにて、コンテナにContentView型が登録される
2. App. CreateShellメソッドにて、MainWindowが生成される
3. MainWIndow.InitializeComponentメソッドが実行される
4. MainWIndow.xamlの下記記述を元にMainWindowModelが生成される
「prism:ViewModelLocator.AutoWireViewModel="True" 」
5. MainWindowModelのコンストラクタで"ContentRegion"とContentView型が紐づけられる
6. MainWindow.DataContextにMainWindowModelが代入される
7. MainWindow.xamlのContentControlにて"ContentRegion"で検索されたContentViewが生成される
8. 画面に数字の1が表示される

f:id:kazoojapan1985:20200705174151p:plain

.NETCore用Prismテンプレートの構成確認(1)

きっかけ

とある記事をよんでいて
.NET FrameworkはプラットフォームがWIndowsだけなのに対して
.NET CoreはWindowsLinuxmacOSに対応しているということを知りました

.NET Coreはオープンソースなのでdll参照した先で何が行われているかも
調べられるので実装時に動作をチェックする回数も減らせそうです

デスクトップ開発にて、いずれ利用することもあると思いますので
Prism Full App(.NET Core)プロジェクトテンプレートを調査してみました
f:id:kazoojapan1985:20200520070924p:plain

構成

Prism Full App(.NET Core)プロジェクトテンプレートで
新規プロジェクトを作成するといきなり次の画面がでました
f:id:kazoojapan1985:20200520071855p:plain

コンテナをDrylocとUnityから選択できるようになっていたので Unityを選択しました(Drylocってなんなんだろう)

出来上がった初期状態のプロジェクトがこちらになります
f:id:kazoojapan1985:20200520072812p:plain

プロジェクトは6つのモジュールで構成されていました

アプリ名.Modules.ModuleName

メイン画面にRegion登録するコントロールであるViewAをデザインする場所みたい
MVVM的にはViewとViewModelを登録するみたい

アプリ名.Services

実質Modelみたいなものかなと思います

MVVMを作っているとModel部分のビジネスロジックも使いまわしたくなってきます
流用したいビジネスロジックを用意しておくところと思われます

アプリ名.Services.Interfaces

アプリ名.Servicesをどこからでも弱参照で利用できるようにコンテナ解決して
利用するためのインターフェースかなと思います

アプリ名.Modules.ModuleName.Tests

アイコンが違いますがテスト用のモジュールですかね
ぱっと見わからんちん

アプリ名

Main()を生成するAppクラスとメイン画面が用意されてます
土台

アプリ名.Core

リージョン名を設定する項目と
リージョン登録に対応するViewModelの基底クラスが用意されています

まとめ

大雑把にみるとわかりやすくまとまったテンプレートだなとおもいました

WPFのMVVMで電卓づくり(15)

指定した有効桁数で出力画面の数値を表示し
桁あふれを起こしたらエラー表示させる機能を実装します

構想

MainModelにて桁あふれを起こしたらエラー表示させる機能を実装します

実装

構想に沿って各MVVMクラスに追記していきます

MainModel

有効桁数のフィールドを固定値で設定します

       // 有効桁数
        private const int DIGITS = 12;

数字ボタンクリックでMainViewModel.PushCommand経由で
呼び出されるMainModel.SetText()にて現在の桁数が
DIGITSを超える場合、末尾への数字の追加をスルーするように追記します

       public void SetText(string str)
        {
            if (this.Operation == OperationKind.Equal
             || this.Operation == OperationKind.GrandTotal)
            {
                this.DisplayText = "0";
                this.Operation = OperationKind.None; //バグ修正
            }
            else 
            {
                this.DisplayText = this.IsAdditional ? this.AffectText : this.OriginalText;
            }

            if (this.DisplayText.Equals("0"))
            {               
                if (str.Equals("0") || str.Equals("00"))
                {
                    return;
                }
                if (!str.Equals("."))
                {
                    this.DisplayText = string.Empty;
                }
            }

            // すでに小数点が付与されていたら何もしない
            if (str.Equals(".") && this.DisplayText.Contains(".")) 
            {
                return;
            }

            // データの入力桁数が規定より多い場合何もしない
            if (this.DisplayText.Replace("-", "").Length >= DIGITS)
            {
                return;
            }
            this.DisplayText += str;

            if (this.IsAdditional)
            {
                this.AffectText = this.DisplayText;
            }
            else
            {
                this.OriginalText = this.DisplayText;
            }
        }

四則演算子ボタンクリックでMainViewModel.PushCommand経由で
呼び出されるMainModel.Calculate()のswitch-case文にて計算結果の桁数が
DIGITSを超えているかチェックするメソッドCheckDigits()を呼び出します

               // 四則演算
                case OperationKind.Plus:
                    originalValue += affectValue;
                    originalValue = CheckDigits(originalValue);
                    this.OriginalText = originalValue.ToString("G17");
                    this.AffectText = "0";
                    this.DisplayText = this.OriginalText;
                    break;
                case OperationKind.Minus:
                    originalValue -= affectValue;
                    originalValue = CheckDigits(originalValue);
                    this.OriginalText = originalValue.ToString("G17");
                    this.AffectText = "0";
                    this.DisplayText = this.OriginalText;
                    break;
                case OperationKind.Multiply:
                    originalValue *= affectValue;
                    originalValue = CheckDigits(originalValue);
                    this.OriginalText = originalValue.ToString("G17");
                    this.AffectText = "0";
                    this.DisplayText = this.OriginalText;
                    break;
                case OperationKind.Divide:
                    if (affectValue == 0)
                    {
                        originalValue = 0;
                        this.ErrorText = "E";
                    }
                    else
                    {
                        originalValue /= affectValue;
                    }
                    originalValue = CheckDigits(originalValue);
                    this.OriginalText = originalValue.ToString("G17");
                    this.AffectText = "0";
                    this.DisplayText = this.OriginalText;
                    break;
                default:
                    break;

CheckDigits()を作成します

       public double CheckDigits(double doubleValue)
        {
            string sign = doubleValue < 0D ? "-" : string.Empty;
            long longValue = (long)doubleValue;
            var longAbsText = Math.Abs(longValue).ToString();
            var doubleAbsText = Math.Abs(doubleValue).ToString("F" + DIGITS);
            if (longAbsText.Length > DIGITS)
            {
                // オーバーフロー
                this.ErrorText = "E";
                // オーバー桁数
                var overDigits = longAbsText.Length - DIGITS;
                longAbsText = longAbsText.Insert(overDigits, ".");
                // .より左側が有効桁数を超えた部分
                return double.Parse((sign + longAbsText).Substring(0, DIGITS));
            }
            else if (doubleAbsText.Length > DIGITS)
            {
                return double.Parse((sign + doubleAbsText).Substring(0, DIGITS));
            }
            else
            {
                return doubleValue;
            }
        }

CheckDigits()では、有効桁数の範囲での最大値を超えるとエラー表示させます
また、小数点以下の有効桁数を超える微細な値を切り捨てています
※有効桁数は符号を除いて12桁にしています

また、割愛しますがdouble型からstring型に変換して代入している個所を
下記のようなパターンに置き換えることで、「GT」や「%±」呼び出し時等にも
桁あふれによるエラー表示ができるようにしています

                           //this.DisplayText = GrandTotalValue.ToString();
                            var grandTotalValue = CheckDigits(this.GrandTotalValue);
                            this.DisplayText = grandTotalValue.ToString("G17");

リファクタリング不足で少し冗長なのはご愛嬌ということでお願いします

動作確認

「999999999999 x 12345 =」と入力した際の画面表示
f:id:kazoojapan1985:20200508004017p:plain 「0.9999999999 x 0.12345 =」とクリックした際の画面表示
f:id:kazoojapan1985:20200508002143p:plain

余談

今回で一旦電卓としての機能の実装は終わりにしたいと思います

参考にした電卓「HS-1220TSG」には他にも機能がありますが
簿記の試験でも受けない限り使わないものばかりですのでスルーします

次回からは、気が向いたら電卓の入力履歴を自動でログ出力する機能でも
作ってみようと思います

WPFのMVVMで電卓づくり(14)

百分率で計算するパーセントボタン「%±」を実装します

構想

MainViewにてエラー時無効となるパーセントボタン「%±」を実装します
MainModelにてパーセントボタンに対応した内部データの更新処理を実装します

実装

構想に沿って各MVVMクラスに追記していきます

MainView

パーセントボタン「%±」を実装します

     <!--パーセントボタン-->
        <Button Grid.Row="1" Grid.Column="4" Content="%±" CommandParameter="Percent"/>

尚、前回までの実装で既にボタンのStyleにて
IsEnabledにエラー時falseとなるプロパティを紐づけしてあります

MainModel

「%±」ボタンクリックでMainViewModel.PushCommand経由で
呼び出されるMainModel.SetOperation()にて
switch-case文に「%±」ボタン用の処理を追記します

                   case OperationKind.Percent:
                        if (this.IsAdditional)
                        {
                            if (double.TryParse(this.OriginalText, out double originalValue)
                             && double.TryParse(this.AffectText, out double affectValue))
                            {
                                switch (this.Operation) 
                                {
                                    case OperationKind.Plus:
                                    case OperationKind.Minus:
                                        affectValue = originalValue * affectValue / 100D;
                                        break;
                                    case OperationKind.Multiply:
                                    case OperationKind.Divide:
                                        affectValue /= 100D;
                                        break;
                                    default:
                                        return;
                                }
                                this.AffectText = affectValue.ToString();
                                this.DisplayText = this.AffectText;
                                Calculate(this.Operation);
                                SetOperationText(ope);
                            }
                        }
                        break;

動作確認

起動後「2」「0」「0」「+」「5」「%±」とクリックした際の画面表示
f:id:kazoojapan1985:20200506220850p:plain 起動後「2」「0」「0」「×」「5」「%±」とクリックした際の画面表示
f:id:kazoojapan1985:20200506220853p:plain

WPFのMVVMで電卓づくり(13)

計算結果の総計を集計するボタン「GT」を実装します
「GT」ボタン1回クリックで現時点での総計を表示させます 「GT」ボタン2回クリックで現時点での総計を破棄させます

構想

MainViewにてエラー時無効となる集計ボタン「GT」とGTマーク表示用画面を実装します
MainModelにて集計ボタンに対応した内部データの更新処理を実装します

実装

構想に沿って各MVVMクラスに追記していきます

MainView

集計ボタン「GT」を実装します

     <!--総計ボタン-->
        <Button Grid.Row="1" Grid.Column="5" Content="GT" CommandParameter="GrandTotal"/>

尚、前回までの実装で既にボタンのStyleにて
IsEnabledにエラー時falseとなるプロパティを紐づけしてあります

「GT」ボタンが有効であることを示すGTマーク表示用画面を出力画面の隣に実装します

     <Grid>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <!--GT表示-->
            <TextBlock Text="{Binding Path=GrandTotalText}"/>
            <!--エラー表示-->
            <TextBlock Grid.Row="1" Text="{Binding Path=ErrorText}"/>
            <!--四則演算子表示-->
            <TextBlock Grid.Row="2" Text="{Binding Path=OperationText}"/>
        </Grid>
        <!--出力画面-->
        <TextBlock Grid.Column="1" Grid.ColumnSpan="5"
                   Text="{Binding Path=DisplayText}" FontSize="80"/>

MainViewModel

GTマーク更新用プロパティを用意します

       public string GrandTotalText { get; set; }

MainViewと紐づけしたプロパティがずいぶん増えてきましたので
UpdateView()を作成してひとまとめにします

       public MainViewModel()
        {
            model = new MainModel();
            UpdateView();

            this.PushCommand = new DelegateCommand(
                param => 
                {
                    if (param == null) { return; }
                    var str = param.ToString();
                    if (model != null)
                    {
                        if (int.TryParse(str, out int num))
                        {
                            model.SetText(str);
                        }
                        else
                        {
                            model.SetOperation(str);
                        }
                        UpdateView();
                    }
                },
                param =>
                {
                    return true;
                });
        }
        public void UpdateView()
        {
            if (model == null) { return; }
            this.DisplayText = model.DisplayText;
            RaisePropertyChanged(nameof(this.DisplayText));
            this.GrandTotalText = model.GrandTotalValue != 0D ? "GT" : string.Empty;
            RaisePropertyChanged(nameof(this.GrandTotalText));
            this.ErrorText = model.ErrorText;
            RaisePropertyChanged(nameof(this.ErrorText));
            this.IsButtonEnable = string.IsNullOrEmpty(this.ErrorText) ? true : false;
            RaisePropertyChanged(nameof(this.IsButtonEnable));
            this.OperationText = model.OperationText;
            RaisePropertyChanged(nameof(this.OperationText));
        }

MainModel

集計用のプロパティを用意します

       // 総計値
        public double GrandTotalValue { get; private set; } = 0D;

また、「=」クリック時に集計用プロパティへの加算を行う処理を追記します

                   case OperationKind.Equal:
                        if (this.IsAdditional)
                        {
                            Calculate(this.Operation);
                            this.IsAdditional = false;
                        }
                        else 
                        {
                            this.OriginalText = "0";
                        }
                        if (double.TryParse(this.DisplayText, out double equalValue))
                        {
                            this.GrandTotalValue += equalValue;
                        }
                        SetOperationText(ope);
                        this.Operation = OperationKind.Equal;
                        break;

「GT」ボタンクリックでMainViewModel.PushCommand経由で
呼び出されるMainModel.SetOperation()にて
switch-case文に「GT」ボタン用の処理を追記します

                   case OperationKind.GrandTotal:
                        if (this.Operation == OperationKind.GrandTotal)
                        {
                            Reset(OperationKind.Clear);
                            this.OriginalText = this.GrandTotalValue.ToString();
                            this.DisplayText = this.OriginalText;
                            this.GrandTotalValue = 0D;
                            SetOperationText(ope);
                            this.Operation = OperationKind.None;
                        }
                        else
                        {
                            this.DisplayText = this.GrandTotalValue.ToString();
                            this.Operation = OperationKind.GrandTotal;
                        }
                        break;

SetOperationText()に「GT」ボタンを2回クリックした際に「GT」表示を消す処理を追記します

               case OperationKind.GrandTotal:
                    if (this.Operation == OperationKind.GrandTotal)
                    {
                        this.OperationText = string.Empty;
                    }
                    break;

「GT」ボタン2回クリック後、数字ボタンを押した際に出力画面との整合性がとれるようにします

       public void SetText(string str)
        {
            if (this.Operation == OperationKind.Equal
             || this.Operation == OperationKind.GrandTotal)
            {
                this.DisplayText = "0";
            }
            else 
            {
                this.DisplayText = this.IsAdditional ? this.AffectText : this.OriginalText;
            }

            if (this.DisplayText.Equals("0"))
            {               
                if (str.Equals("0") || str.Equals("00"))
                {
                    return;
                }
                if (!str.Equals("."))
                {
                    this.DisplayText = string.Empty;
                }
            }

            // すでに小数点が付与されていたら何もしない
            if (str.Equals(".") && this.DisplayText.Contains(".")) 
            {
                return;
            }

            this.DisplayText += str;

            if (this.IsAdditional)
            {
                this.AffectText = this.DisplayText;
            }
            else
            {
                this.OriginalText = this.DisplayText;
            }
        }

動作確認

起動後「1」「+」「2」「=」「+」「4」「=」「GT」とクリックした際の画面表示
f:id:kazoojapan1985:20200506205356p:plain 続けて「GT」「+」「5」「=」「GT」とクリックした際の画面表示
f:id:kazoojapan1985:20200506205416p:plain

WPFのMVVMで電卓づくり(12)

出力画面の符号を反転させるボタン「±」を実装します

構想

MainViewにてエラー時無効となる符号反転ボタン「±」を実装します
MainModelにて符号反転ボタンに対応した内部データの更新処理を実装します

実装

構想に沿って各MVVMクラスに追記していきます

MainView

符号反転ボタン「±」を実装します

     <!--符号反転ボタン-->
        <Button Grid.Row="3" Grid.Column="0" Content="±" CommandParameter="Reverse"/>

尚、前回までの実装で既にボタンのStyleにて
IsEnabledにエラー時falseとなるプロパティを紐づけしてあります

MainModel

符号反転ボタンクリックでMainViewModel.PushCommand経由で
呼び出されるMainModel.SetOperation()にて
switch-case文に符号反転ボタン用の処理を追記します

                   case OperationKind.Reverse:
                        Reverse();
                        break;

出力画面の符号を反転させる処理Reverse()を作成します

       public void Reverse()
        {
            if (this.IsAdditional)
            {
                if (this.AffectText.Contains("-"))
                {
                    this.AffectText = this.AffectText.Substring(1);
                }
                else
                {
                    if (!this.AffectText.Equals("0"))
                    {
                        this.AffectText = "-" + this.AffectText;
                    }
                }
                this.DisplayText = this.AffectText;
            }
            else
            {
                if (this.OriginalText.Contains("-"))
                {
                    this.OriginalText = this.OriginalText.Substring(1);
                }
                else
                {
                    if (!this.OriginalText.Equals("0")) 
                    {
                        this.OriginalText = "-" + this.OriginalText;
                    }
                }
                this.DisplayText = this.OriginalText;
            }
        }

出力画面が0のときは符号を反転させないようにしています

動作確認

起動後「±」とクリックした際の画面表示
f:id:kazoojapan1985:20200506174740p:plain 起動後「・」「±」「1」とクリックした際の画面表示
f:id:kazoojapan1985:20200506174808p:plain

WPFのMVVMで電卓づくり(11)

数字ボタンと浮動小数点ボタンのクリックをキャンセルするボタン「→」を実装します

構想

MainViewにてエラー時無効となるキャンセルボタン「→」を実装します
MainModelにてキャンセルボタンに対応した内部データの更新処理を実装します

実装

構想に沿って各MVVMクラスに追記していきます

MainView

キャンセルボタン「→」を実装します

     <!--キャンセル(戻る)ボタン-->
        <Button Grid.Row="2" Grid.Column="0" Content="→" CommandParameter="Undo"/>

尚、前回までの実装で既にボタンのStyleにて
IsEnabledにエラー時falseとなるプロパティを紐づけしてあります

MainModel

キャンセルボタンクリックでMainViewModel.PushCommand経由で
呼び出されるMainModel.SetOperation()にて
switch-case文にキャンセルボタン用の処理を追記します

                   case OperationKind.Undo:
                        Undo();
                        SetOperationText(ope);
                        break;

数字ボタンと浮動小数点ボタンのクリックをキャンセルする処理Undo()を作成します

       public void Undo() 
        {
            if (this.IsAdditional)
            {
                this.AffectText = this.AffectText.Substring(0, this.AffectText.Length - 1);
                if (string.IsNullOrEmpty(this.AffectText)
                 || this.AffectText.Equals("-"))
                {
                    this.AffectText = "0";
                }
                this.DisplayText = this.AffectText;
            }
            else
            {
                this.OriginalText = this.OriginalText.Substring(0, this.OriginalText.Length - 1);
                if (string.IsNullOrEmpty(this.OriginalText)
                 || this.OriginalText.Equals("-"))
                {
                    this.OriginalText = "0";
                }
                this.DisplayText = this.OriginalText;
            }
        }

動作確認

起動後「-」「・」「5」「=」とクリックした際の画面表示
f:id:kazoojapan1985:20200506170245p:plain
続いて「→」とクリックした際の画面表示
f:id:kazoojapan1985:20200506170917p:plain
続いて「→」とクリックした際の画面表示
f:id:kazoojapan1985:20200506170955p:plain
※参考にした電卓「HS-1220TSG」とは若干挙動が異なってます