redwarrior’s diary

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

WPFアプリのWindowの閉じる・開くを、MVVMフレームワークを使用して実装する

WPFアプリケーションで、自作のボタンを押して画面を閉じて、新しい画面を表示する処理をMVVMフレームワークを使用して実装する方法を調べました。

MVVMフレームワークは、Prismを使用します。

自作のボタンを押して画面を閉じる

自作のボタンを押して画面を閉じるやり方です。これは、以下で紹介されているCommandParameter属性を使用する方法にしました。WindowクラスをViewModelが参照するので、若干気になる部分ではありますが、引数なので良しとし、コードビハインドを使用しないという方向で決めました。

c# - Close Window from ViewModel - Stack Overflow

Windowにx:Nameで名前を定義するのではなく、RelativeSourceを使用して自分の親Windowを取得するようにします。

XAML

<Button Width="120" Height="30"
        Command="{Binding OpenCommand}"
        CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}">
    Open New Window
</Button>

ViewModel

public DelegateCommand<Window> OpenCommand { get; set; }

public MainWindowViewModel()
{
    OpenCommand = new DelegateCommand<Window>(ExecuteOpen);
}

private void ExecuteOpen(Window window)
{
    window?.Close();
}

新しい画面を表示する

続いて新しい画面を表示する方法です。新しい画面を表示しないとアプリケーションが終了してしまいますからね。

実は、(呼び出し元の)ViewModelから(呼び出し先の)Windowをnewして、Showメソッドを呼び出すだけで画面表示そのものは出来てしまいます。PrismがViewModelの紐づけや、DIを行ってくれます。
しかし、メソッド内で特定のView(Window)をnewして、Showメソッドまで呼び出しているので、上とは違って、このままではMVVMとして良くありませんし、単体テストも難しいです。

以下のサイトを参考にして、解決しました。

c# - Open a new Window in MVVM - Stack Overflow

Opening new window in MVVM WPF

WindowをnewしてShowメソッドを呼び出す処理をメソッドにし、画面表示用のクラスを用意して、そこにメソッドを移動します。
ViewModelからは、画面表示用クラスのメソッドを呼び出します。画面表示用のクラスは、DIで取得します。

画面表示用クラス

public class RouteService : IRouteService
{
    private readonly IContainerExtension _container;

    public RouteService(IContainerExtension container)
    {
        _container = container;
    }

    public void ShowBrandNewWindow(string username)
    {
        var brandNewWindow = _container.Resolve<BrandNewWindow>();
        brandNewWindow.Show();
    }
}

ViewModel

public DelegateCommand<Window> OpenCommand { get; set; }

public MainWindowViewModel(IRouteService routeService)
{
    _routeService = routeService;

    OpenCommand = new DelegateCommand<Window>(ExecuteOpen);
}

private void ExecuteOpen(Window window)
{
    _routeService.ShowBrandNewWindow(Username);

    window?.Close();
}

開発環境:

  • .NET Core 3.1

  • Prism 7.2.0.1422

おまけ

今回と一つ前の内容をサンプルプロジェクトとして、GitHubリポジトリに追加したので、全体像を見たい方はどうぞ。

github.com

以上。

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の修正が必要になります。

以上

動作環境: