redwarrior’s diary

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

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

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

以上。

動作環境:

ASP.NET MVCでjQuery DataTablesを使用する

何番煎じなのかわかりませんが、jQuery DataTablesを使ってみたので、メモを残しておきます。試したバージョンは、1.10.12。

公式サイト:

datatables.net

ASP.NET MVCでは、NuGetを使用して導入します。jQueryはバージョン2系でも問題ありませんでした。バージョン3系は試していません。

www.nuget.org

基本的な使い方

thead、tbodyタグで作成したテーブルのtableタグにid属性(mytable)を設定し、画面上もしくは外部のjsファイルで、以下のようにidを指定してDataTableメソッド呼び出すだけです。

$(function () {
    $('#mytable').DataTable();
});

ASP.NET MVCでは、jsファイルやcssファイルはバンドル機能を使って配布されるので、BundleConfig.csに追記しないと参照できません。

bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
          "~/Scripts/bootstrap.js",
          "~/Scripts/respond.js",
          "~/Scripts/DataTables/jquery.dataTables.js",      //←追加
          "~/Scripts/DataTables/dataTables.bootstrap.js")); //←追加

bundles.Add(new StyleBundle("~/Content/css").Include(
          "~/Content/bootstrap.css",
          "~/Content/DataTables/css/dataTables.bootstrap.css", //←追加
          "~/Content/site.css"));

日本語化

CDN上のファイルを参照する方法が良く紹介されていますが、社内システム等で外部へのアクセスを避けたい場合もあるので、CDNを使わない方法で行きます。

先ほどのDataTableメソッドの前に以下を記述することで、デフォルトメッセージを日本語で上書きします。

    // デフォルトの設定を変更
    $.extend($.fn.dataTable.defaults, {
        language: {
            "sEmptyTable": "テーブルにデータがありません",
            "sInfo": " _TOTAL_ 件中 _START_ から _END_ まで表示",
            "sInfoEmpty": " 0 件中 0 から 0 まで表示",
            "sInfoFiltered": "(全 _MAX_ 件より抽出)",
            "sInfoPostFix": "",
            "sInfoThousands": ",",
            "sLengthMenu": "_MENU_ 件表示",
            "sLoadingRecords": "読み込み中...",
            "sProcessing": "処理中...",
            "sSearch": "検索:",
            "sZeroRecords": "一致するレコードがありません",
            "oPaginate": {
                "sFirst": "先頭",
                "sLast": "最終",
                "sNext": "次",
                "sPrevious": "前"
            },
            "oAria": {
                "sSortAscending": ": 列を昇順に並べ替えるにはアクティブにする",
                "sSortDescending": ": 列を降順に並べ替えるにはアクティブにする"
            }
        }
    });
    $('#mytable').DataTable();

sSearchの値が、検索テキストボックスの左側の文字列ですが、画面よって変えたい場合もあると思います。その場合は、DataTableメソッド呼び出し時の引数で指定することが出来ます。

    $('#mytable').DataTable({
        language: {
            "sSearch": "絞り込み:"
        }
    });

列の設定

特定の列を検索対象から外したり、ソートできないようにしたり、非表示にしたりしたい場合は、columnDefsというプロパティに設定します。

    $('#mytable').DataTable({
        language: {
            "sSearch": "絞り込み:"
        },
        columnDefs: [
            //targetsは0始まり
            { targets: 0, width: 43, searchable: false, orderable: false }, //1列目 検索対象外、ソート不可
            { targets: 1, width: 85, orderable: false }, //2列目 検索対象、ソート不可
            { targets: 2, visible: false }, //3列目 非表示
        ]
    });

パフォーマンスについて

HTMLを全て読み込んだ後、JavaScriptでページネーションや検索の制御をしているため、表示するレコード数が多いほど遅くなります。

試してみたところ12列の表で、1000行くらいまでがストレスなく表示できる感じでした。

以上です。