redwarrior’s diary

C#, ASP.NET, WEB, G*などの雑多な情報

WPFアプリのMainWindowの終了のキャンセルを、MVVMフレームワークを使用して実装する

WPFアプリケーションで、×ボタンを押してメイン画面を閉じるときに、ある条件を満たす場合は閉じないようにしたい場合があります。言い換えると、終了のキャンセルをしたい場合があります。

コードビハインドを使えば、簡単に実装する事が出来ますが(参考: C#のWPFでWindowを閉じる前に確認する - Ararami Studio)、
MVVMフレームワークを使用している場合は、ViewModelクラスで処理を行いたいです。

色々と調べてやってみたところ、MVVMフレームワークPrismを使用して実装する事が出来たので、やり方を残しておきます。

ボタンも配置していないプロジェクト作成直後の画面を使用します。

f:id:redwarrior:20200805165803p:plain

XAML

<Window x:Class="WindowCloseCancel.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/"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="{Binding Title}" Height="350" Width="525" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closing">
            <prism:InvokeCommandAction Command="{Binding ClosingCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <ContentControl prism:RegionManager.RegionName="ContentRegion" />
    </Grid>
</Window>

i:EventTriggerタグとprism:InvokeCommandActionタグを使用する事で、ClosingイベントをClosingCommandプロパティに紐づけています。

ViewModel

public class MainWindowViewModel : BindableBase
{
    private string _title = "Prism Application";

    public string Title
    {
        get { return _title; }
        set { SetProperty(ref _title, value); }
    }

    public DelegateCommand<CancelEventArgs> ClosingCommand { get; set; }

    public MainWindowViewModel()
    {
        ClosingCommand = new DelegateCommand<CancelEventArgs>(ExecuteClosing);
    }

    private void ExecuteClosing(CancelEventArgs e)
    {
        e.Cancel = true;
    }
}

ジェネリックなDelegateCommandクラスの型パラメータで指定したものが、コンストラクタの引数のActionクラスの型パラメータにも指定され、Actionクラスに代入するメソッドでは、引数として型パラメータで指定したオブジェクトを受け取る事が出来ます。

言葉にするとややこしいですが、コードビハインドと同様にメソッドを記述できるという事です。今回は、CancelEventArgsだけを使用しましたが、型パラメータを2つ指定すれば、object sender を受け取る事も出来ます。

コードビハインドと同様に、CancelEventArgsクラスのCancelプロパティをtrueにすることで、終了をキャンセルする事が出来ます。

i:EventTriggerタグとprism:InvokeCommandActionタグ、DelegateCommandクラスの組み合わせは、他のイベントでも使用できるため、使い勝手があがりそうです。

.NET Framework、.NET Coreの両方の環境で動作することを確認しました。

開発環境:

  • .NET Core 3.1

  • Prism 7.2.0.1422

以上

2重起動防止処理を組み込んだWPFアプリでClickOnceの更新と再起動を行う

タイトルでやりたい事を伝えきったので、やり方を説明します。

ClickOnceの更新方法は、探せば見つかると思いますので割愛します。

参考:ClickOnce に関するまとめ - Qiita

ClickOnceによる更新は、アプリケーションを再起動しないと行われません。
なので、ClickOnceの更新メソッドを呼び出した後に、以下のコードを実行して、アプリケーションを再起動します。

System.Windows.Forms.Application.Restart();
System.Windows.Application.Current.Shutdown();

一方、WPFアプリケーションで2重起動を防止したい時は、App.xaml.csを編集し、Mutexを使用して以下のように記述すると思います。

参考:WPFでアプリの2重起動を禁止する。 - プログラムを書こう!

private static readonly Mutex Mutex = new Mutex(false, "Mutex名");
private static bool _hasHandle = false;

private void OnStartup(object sender, StartupEventArgs e)
{
    _hasHandle = Mutex.WaitOne(0, false);

    if (!_hasHandle)
    {
        MessageBox.Show("2重起動はできません。");

        Shutdown();
        return;
    }
}

private void OnExit(object sender, ExitEventArgs e)
{
    if (_hasHandle)
    {
        Mutex.ReleaseMutex();
    }

    Mutex.Close();
}

ここで終了確認をしようと思って、終了プロセスのどこかで、MessageBox.Showによるメッセージ表示を組み込んだとします。

その状態で、ClickOnceの更新+アプリケーションの再起動をすると、終了確認のメッセージに加えて、なんと「2重起動はできません。」というメッセージが表示されてしまいました。

これは終了処理が「終了確認のメッセージを表示する」ところで止まったため、Mutexの解放をする前に新しいアプリケーションを起動しようとして、2重起動防止処理が動いてしまった結果です。

パラメータ等による分岐で、更新時は終了確認のメッセージ表示をやめることで、とりあえず動作するようにはなりました。

しかし、終了処理に時間がかかるようになれば、Mutexの解放をする前に新しいアプリケーションが起動する可能性が残っています。

これを解消するために、最終的にはアプリケーションの起動処理をMutexの解放後にもっていきました。

ViewModel

IsRestart = true;        //ViewModelでの終了処理の前に、再起動フラグを設定する
Application.Current.Shutdown();

App.xaml.cs

private void OnExit(object sender, ExitEventArgs e)
{
    // Mutexの所有権を保持しているプロセスが実行する
    if (_hasHandle)
    {
        // Mutexを解放します
        Mutex.ReleaseMutex();
    }

    // Mutexのクローズ処理
    // 両方のプロセスで実行される
    Mutex.Close();

    // Mutexの解放後に、新しいアプリケーションを起動する
    var viewModel = (MainWindowViewModel) _mainWindow.DataContext;
    if (viewModel.IsRestart)
    {
        // 新しいアプリケーションの起動(このメソッドでは、WPFアプリケーションは終了しない)
        System.Windows.Forms.Application.Restart();
    }
}

これによって、2重起動防止処理を組み込んだ状態で、ClickOnceの更新と再起動を行うことが出来ました。

以上。

エラーメッセージがViewを切り替えると消える事への対処方法

課題

f:id:redwarrior:20200718152439p:plain
バリエーションエラー

一つ前の記事で、第5回のエラー処理のサンプルを作成した時に、バリエーションによるエラーメッセージを表示した状態で、一覧から別の項目を選択すると、Viewが切り替わり、右側に新しい画面が表示される。
その後、元の項目を表示すると再びViewが切り替わり、バリエーションエラーが発生している画面が表示される。

この時、切り替え前には表示されていたエラーメッセージが消えてしまう。入力フォームに赤い枠があるため、バリエーションエラーは発生しているのがわかる。

AdornedElement の関連の設定だと思い、色々と調べた結果、Viewが切り替わってもエラーメッセージが消えない方法を編み出した。

エラー処理で検索してヒットするのは、WPF コントロール(TextBox)等のValidation.ErrorTemplate プロパティに、リソースとして定義しておいたControlTemplateを設定する方法だが、
この方法だと上記の通りViewが切り替わるとエラーメッセージが消えてしまう。

参考: Nine Works INotifyDataErrorInfoを使用したデータ検証

そこで、Validation.ErrorTemplate への設定方法を変えると上手くいく。

対処方法

<UserControl.Resources>
    <Style x:Key="ErrorStyle" TargetType="TextBox">
        <Setter Property="Margin" Value="4" />
        <Style.Triggers>
            <Trigger Property="IsVisible" Value="True">
                <Setter Property="Validation.ErrorTemplate">
                    <Setter.Value>
                        <ControlTemplate>
                            <DockPanel>
                                <ItemsControl DockPanel.Dock="Bottom" Margin="5,0"
                                              ItemsSource="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)}">
                                    <ItemsControl.ItemTemplate>
                                        <DataTemplate>
                                            <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
                                        </DataTemplate>
                                    </ItemsControl.ItemTemplate>
                                </ItemsControl>
                                <Border BorderBrush="Red" BorderThickness="1"
                                        Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
                                        Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
                                    <AdornedElementPlaceholder Name="adornedElement"/>
                                </Border>
                            </DockPanel>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>
    </Style>
</UserControl.Resources>

コントロールの属性として指定するのではなく、Styleに設定し、条件を満たしたときに反映されるようにする。

具体的には、Style.Triggerを使用して指定する。トリガーの条件は「IsVisible」プロパティが 「true」になっている事。
条件を満たした場合、Validation.ErrorTemplate にControlTemplateを設定するようにすると、Viewを切り替えてもエラーメッセージが表示される。

使用する場合は、Styleプロパティに設定する。

<TextBox Text="{Binding Author, UpdateSourceTrigger=PropertyChanged}" Width="200" Margin="5,0,5,20" HorizontalAlignment="Left"
         Style="{StaticResource ErrorStyle}"></TextBox>

ちなみに、エラーが発生しているかどうかはコントロールが持っており、エラーがなくなるとValidation.ErrorTemplate が空になってメッセージは消える。

Prismを使用したWPFアプリケーションのまとまったサンプルを作ってみた

新しく作成するアプリケーションで、Windows Forms、WPFで開発するか悩んでいるチームがあったので、 説明用にWPFのサンプルを作成したが、お披露目する前にWindows Formsで開発する事に決まったようなので、勿体無いのでこちらで公開しようと思う。

サンプルは14個。Prism 7系を使用している。第1回~第11回が続きもので少しずつ機能を追加している。EX1~EX3は独立したものになっている。

サンプル全体のリポジトリのURLは以下を参照。

https://github.com/frepe2013/PrismGenerallySample

タイトル
第1回 BindableBaseについて(Prism未使用)(Prism使用後
第2回 Region & Navigationについて
第3回 DelegateCommand & InvokeCommandActionについて
第4回 EntityFrameworkを使用する
第5回 ErrorsContainerについて(ErrorsContainer未使用)(ErrorsContainer使用
第6回 DataAnnotationsValidateを使用する
第7回 StateBasedNavigationについて
第8回 PopupWindowActionについて(ShowDialog版)(PopupWindowAction版
第9回 IValueConverterについて
第10回 Repositoryクラスを使用する
第11回 Serviceクラスを使用する
EX1 DataGridを使用する
EX2 Window間でパラメータを渡す
EX3 Wizard形式のアプリを作成する

本サンプルを作成するにあたって、以下のサイトを参考にさせて頂いた。

以上。

OpenJDKをインストールしているのに、「Java 1.8(Java 8)が必要です」みたいなメッセージが表示される場合の対処方法

まさかの半年遅れで更新。すいません。しかもこの記事ほぼ完成していたのに、下書きのままになっていた。

以下の本を購入したので、試しています。

実際にOWASP ZAPを使ってみようとインストールして起動すると、「Java 1.8(Java 8)が必要です」みたいなメッセージが表示されて、Oracleのホームページに遷移した。

AdoptOpenJDKインストールしてあるのに、「これじゃダメなのか」と思いつつ、OracleJREをダウンロードしてインストールしたら、起動した。

AdoptOpenJDKのバージョンが少し古かったので、最新バージョンをダウンロードしてインストールしようとしたら、インストールメニューの中に「JavaSoft(Oracle) registry keys」というものがあり、インストールされないようになっていた。

もしかして?と思い、OWASP ZAPもOracle JREもアンインストールして、AdoptOpenJDKで上記を含めてインストールした後に、OWASP ZAPだけをインストールしたら、起動時にメッセージが表示されず、設定が先に進んだ。

少し前のバージョンのastahを再インストールする時にJREを含めない設定にすると似たようなメッセージが表示されたが、astahはインストール時に同梱することが出来たので気にならなかった。

他のJavaを使用している製品でも同じことが起きそうなので、今回のは良い経験になった。

NLogを使用して、ログの出力先を切り替える(TFSのリリース管理を使用する版)

Visual Studioからの発行について、以前に以下の記事を書きました。

redwarrior.hateblo.jp

ひとつ前の記事でリリース管理について書きました。

redwarrior.hateblo.jp

リリース管理を使用して、ログの出力先を切り替えたいと思ったので、やり方を調べました。

ポイントは以下の2つ
  • NLog.Extended を使用して、appSettingsに設定した値を出力先に使用する
  • TFSのリリース管理で、appSettingsの値を置き換える

NLog.Extended を使用して、appSettingsに設定した値を出力先に使用する

NLog.Extendedは、v4.0.0.1を使用しました。

Web.configを以下のように編集します。

<appSettings>
  <add key="webpages:Version" value="3.0.0.0" />
  <add key="webpages:Enabled" value="false" />
  <add key="ClientValidationEnabled" value="true" />
  <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  <add key="LogDir" value="c:/inetpub/logs/AppLogs"/>    <!-- この行を追加 -->
</appSettings>
~中略~
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true">
  <targets>
    <!-- 変更前
    <target xsi:type="File" name="f" fileName="c:/inetpub/logs/AppLogs/app.log"
            layout="${longdate} [${uppercase:${level:padding=-5}}][${logger:shortName=True}] ${message} ${exception:format=tostring}"
            archiveEvery="Day"
            archiveFileName="c:/inetpub/logs/AppLogs/app_{#}.log"
            archiveNumbering="Date"
            archiveDateFormat="yyyyMMdd"
            maxArchiveFiles="30"
            keepFileOpen="true">
    </target>
    -->
    <!-- 変更後 -->
    <target xsi:type="File" name="f" fileName="${appsetting:name=LogDir}/app.log"
            layout="${longdate} [${uppercase:${level:padding=-5}}][${logger:shortName=True}] ${message} ${exception:format=tostring}"
            archiveEvery="Day"
            archiveFileName="${appsetting:name=LogDir}/app_{#}.log"
            archiveNumbering="Date"
            archiveDateFormat="yyyyMMdd"
            maxArchiveFiles="30"
            keepFileOpen="true">
    </target>
  </targets>
  <rules>
    <logger name="*" minlevel="Trace" writeTo="f" />
  </rules>
</nlog>

targetタグのfileName、archiveFileNameを、appSettingsに設定したLogDirを使って書き換えています。

TFSのリリース管理で、appSettingsの値を置き換える

リリース管理のタスクで、XML変数置換にチェックをつけて、変数タブでLogDir変数を追加します。

f:id:redwarrior:20191227194826p:plain

f:id:redwarrior:20191227194456p:plain

これで、TFSでリリースする時にログの出力先を変更することが出来ました。

ちなみに、これに書き換えた後にVisual Studioからの発行する場合は、同様にappSettingsを置き換えるようにWeb.***.configの修正が必要になります。

以上

動作環境:

TFS 2018 のリリース管理を使用して、IISにデプロイする

またも、3か月近く間が空いてしまいました。ネタはたまっているので、間隔をなるべくおかずに書いて行きたいものです。

以下の記事では、ビルド時のオプションで発行まで行いましたが、今回はリリース管理機能を使ってIISにデプロイしてみました。

redwarrior.hateblo.jp

ググってみると、以下のサイトに情報を発見。

stackoverflow.com

IIS Web App ManageIIS Web App Deployというものが用意されているらしい。

リリース定義の作成

リリース定義を新規作成すると、テンプレートの選択で「IIS Web サイトと SQL Database の配置」があったので、これを適用しました。

f:id:redwarrior:20191221154900p:plain

すると、新しいリリース定義の画面になり、タスクタブを表示するとIIS Deploymentが出来て、その下にIIS Web App ManageIIS Web App Deployがあり、IIS Deploymentと同じレベルにSQL Deploymentが出来て、その下にSQL DB Deployがありました。

f:id:redwarrior:20191221154921p:plain

データベースは既に作成済なので、SQL Deploymentは不要。このまま削除しても良いけれど、リリース定義のテンプレートをよく見ると「IIS Web サイトの配置」があったので、このテンプレートでリリース定義を作り直しました。

さらに上記サイト*1によると、IIS Web App Manageは、IISのWebサイト作成に使用するようだ。今回は既に動いているWebアプリケーションのデプロイ方法を変更するので、IISの設定は変更しないため、タスクは削除しました。

f:id:redwarrior:20191221155056p:plain

各種パラメータの設定

必要なパラメータを設定します。

まず、配置プロセスで環境名とWebサイト名を設定した。Webサイト名は、動いているアプリケーションのIISのWebサイト名を設定。

次に、IIS Deploymentで表示名はそのままにして、配置グループの入力が必要になりました。配置グループはまだ作成していなかったので、歯車ボタンか、リリースタブの並びの配置グループをクリックして配置グループを新規作成しました。

f:id:redwarrior:20191221155416p:plain

配置グループ作成後に表示される登録スクリプトをコピーして、配置するサーバ上で「管理者 PowerShell コマンド プロンプト」から実行してもらうとターゲットタブに対象サーバが表示されます。
ちなみに登録スクリプトを表示する場合は、詳細タブを表示するか登録リンクをクリックすればよいです。

f:id:redwarrior:20191221155908p:plain

配置グループを作成すると、配置プールが一緒に作成されます。これは他のプロジェクトでも同じサーバに配置をしたい場合に使用することが出来ます。配置グループの詳細タブの配置プールの管理リンクから表示することが出来ます。

次は、IIS Web App Deployで仮想アプリケーションにアプリケーション名を入力します。パッケージまたはフォルダーはそのままにします。それ以外は特に入力していません。

パイプラインの設定

パイプラインの設定の前に、ビルドの定義を変更し、Web Deployではなく、Web デプロイ パッケージに変更します。 MSBuild引数に以下の値を設定します。

f:id:redwarrior:20191221160219p:plain

設定した値:

/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactstagingdirectory)\\"

PackageLocationで$(build.artifactstagingdirectory)を使用すことで、その後の成果物の発行タスクでビルド結果がTFSに登録されるようにしています。

ハイプラインの設定に戻り、成果物のソースの種類でビルドを選択し、ソースとして、使用しているビルド定義を選択します。

成果物の継続的配置トリガーを有効にし、環境の配置前の条件でトリガーにリリース後を選択する。承認などは設定していません。

f:id:redwarrior:20191221160646p:plain f:id:redwarrior:20191221160659p:plain

上記の設定を行うと、ビルド成功後に、リリース管理機能によって、サーバへのデプロイが行われるようになります。

以上。

動作環境: