redwarrior’s diary

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

ClickOnceで使用できたコード署名が、MSIXで使用できない理由を調査し、対応した

背景

この間、ClickOnceの発行をする時に、使用したコード署名証明書(コードサイニング証明書)をPCの証明書ストアにインストールしたので、 .NET Coreアプリケーションの配布形式であるMSIXでも試してみました。

MSIX形式で配布する方法を調べると、以下のサイトに詳しくのっていました。

devlog.grapecity.co.jp

サイトを参考に、.NET CoreのWPFアプリケーションを作成し、Windows アプリケーション パッケージ プロジェクト(以下、パッケージプロジェクト)をソリューションに追加しました。

パッケージプロジェクトを右クリックして、「公開」⇒「アプリ パッケージの作成」と進んで、「署名方法の選択」画面から、「Store から選択」をクリックします。

この後、証明書の選択ウィンドウが出てくるのですが、画面には「証明書がありません」のメッセージ。

??と思い、現在のソリューションを一旦終了して、.NET Frameworkプロジェクトを開き、プロジェクトのプロパティから署名メニューを選択すると、コード署名証明書はあります。「ストアからの選択」にも表示されます。

本題

ClickOnceで使用できたコード署名が、MSIXで使用できない理由を調べました。

パッケージプロジェクトの画面では、テスト用の証明書を作成する事が出来ます。作成したファイルは、プロジェクトにpfxファイルとして保存されます。

テスト用証明書を作成して、それを証明書ストアにインストールしてみました。その後、「Store から選択」を押してみると、インストールしたテスト用証明書が選択できます。

また、対象のコード署名証明書をファイルにエクスポートして、「ファイルから選択」でエクスポートしたファイルを選択してみました。すると「選択された証明書はコード署名には無効です。」というエラーメッセージが表示されました。

という事は、ClickOnceの発行に使用したコード署名証明書に問題がありそうです。エラーメッセージから検索したり、対象のコード署名証明書とテスト用証明書のフィールドを見比べると以下がわかりました。

フィールド:基本制限(basic constraints, oid=2.5.29.19)の値に Subject Type=End Entity が設定されていない。

アプリケーション パッケージへの署名 (Windows ストア アプリ) | Microsoft Docs

パッケージ署名用証明書を作成する - MSIX | Microsoft Docs

以前にルート証明書について調べたときに、この値はCAかどうかを指定すると書いてあったので、ルート証明書ではないので、Subject Typeは空欄にしていました。前述のようにClickOnceでは使えていました。

以下のように、PowerShellのNew-SelfSignedCertificateコマンドレットを使用して、基本制限を設定したテスト用証明書その2を作成したところ、「Store から選択」に表示されました。

> New-SelfSignedCertificate -FriendlyName "証明書を人が区別するための名前" -DnsName "団体名" -TextExtension @("2.5.29.19={text}") -Type CodeSigningCert -CertStoreLocation "Cert:\CurrentUser\My" -Subject "CN=xxx,O=yyy,OU=zzz,C=JP"

対応

テスト用ではないコード署名証明書は、Active Directory証明書サービス(Active Directory Certificate Services:AD CS)で発行されるため、修正方法を調べて発行しなおしてもらいました。

やったことは、AD CS上でのテンプレートの修正です。具体的には、互換性タブでの対象OSの変更と、拡張機能の基本制限の「有効にする」をチェックしました。

修正後に証明書を更新してもらったところ、新しいコード署名証明書が「Store から選択」に表示されました。

以上

Entity Framework で SQLite を使用する

NuGetでSystem.Data.SQLiteパッケージをインストールし、以下をApp.configに追加する。

  <entityFramework>
    <providers>
      <provider invariantName="System.Data.SqlClient"
                type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
      <provider invariantName="System.Data.SQLite"
                type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
      <provider invariantName="System.Data.SQLite.EF6"
                type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
    </providers>
  </entityFramework>
  <system.data>
    <DbProviderFactories>
      <remove invariant="System.Data.SQLite.EF6" />
      <add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6"
           description=".NET Framework Data Provider for SQLite (Entity Framework 6)"
           type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" />
      <remove invariant="System.Data.SQLite" />
      <add name="SQLite Data Provider" invariant="System.Data.SQLite"
           description=".NET Framework Data Provider for SQLite"
           type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
    </DbProviderFactories>
  </system.data>

リンク先を見てもらえばわかるが、System.Data.SQLiteは依存パッケージが設定されているため、一つインストールすれば必要なものが全て入るようになっている。

www.nuget.org

以上

TFS 2018 で ClickOnce の発行を行う

少し前に作成したWPFアプリケーションは、対象OSとしてWindows 8.1も含まれるため、フレームワークとして.NET Framework 4.5.1を使用しました。

Webアプリ(ASP.NET MVC)は、TFS上でビルド(XAMLではない)、リリースが出来るようにしたので(下記参照)

redwarrior.hateblo.jp

今回はデスクトップアプリケーションのビルド、ClickOnceによる発行をTFS上で試しました。

はじめに

TFSはビルド、リリースに分かれていて、ビルドが対象のビルドを行い、リリースで各環境への配置を行います。 ASP.NET MVCでは、リリースでIISの設定を行うため、ビルドとリリースに分けて使いました。

しかし、ClickOnceは、MSBuildのパラメータでビルド/発行を切り替えるし、発行先は1か所なので、リリースを使用せず、ビルドだけを使用しました。

作業開始

まず、「.NET デスクトップ」テンプレートを使用してビルド定義を作成しました。

WPFアプリケーションで、Prismを使用するためにPrism Template Packでプロジェクトを作成したところ、NuGetパッケージの管理方式に「PackageReference」を使用しており、リポジトリにpackagesフォルダが存在せず、リポジトリに使用するDLLを含めなくなりました。

そのため、NuGet関連タスクは削除せずに残すことにして、早速、ビルドを試しました。これはテンプレートのままで上手くいきました。

次に、ClickOnceの発行を試しました。「ソリューションのビルド」タスクのMSBuild引数で/target:publishとすると、ClickOnce用のファイルが、app.publishフォルダに作成されます。

VSの画面から発行した場合とは違い、サーバへのファイルコピーは行われません。

以前にコマンドラインから実行したときは、csprojファイルを編集して、コピー処理を追加しました。これを今回はTFSのビルド定義のタスクで行う必要があります。

いろいろと試してみた結果、最終的にはファイルのコピー用に、二つのタスクを作成しました。

ファイルのコピータスク

ビルド結果から、app.publishフォルダだけをコピーします

ソースフォルダ
$(system.defaultworkingdirectory)

コンテンツ
**\bin\$(BuildConfiguration)\app.publish\**
!**\bin\$(BuildConfiguration)\app.publish\$(VsProject).exe

ターゲット フォルダー
$(build.artifactstagingdirectory)
成果物の公開タスク

app.publishフォルダを、配布用フォルダにコピーします。発行場所は、「TFS」ではなく「A file share」を指定しました。

発行するためのパス
$(build.artifactstagingdirectory)\$(VsSolution)\bin\$(BuildConfiguration)\app.publish

成果物名
app.pubslihの中身が格納されるフォルダ名

ファイル共有パス
成果物が格納されるフォルダの親フォルダ

VsProject、VsSolution変数は、変数タブで作成し、値としてプロジェクト名、ソリューション名を登録しました。

つまづいた点

最初はファイルのコピータスクで、配布用フォルダへのコピーまでやろうとしたが、ソースフォルダ(リポジトリのルートパス)からのコピーになってしまい、階層が深くなってしまったので、分けることにしました。

成果物の公開タスクで、ファイルが圧縮されてしまわないか心配しましたが大丈夫でした(フォルダの内容がそのままコピーされました)

以上

Entity Framework CoreのInsert高速化 & InsertOrUpdateを行うライブラリ

数万件単位のレコードを登録するバッチアプリケーションを作成することになった。

公開されているWeb APIからデータを取得して、かなり編集をしてから登録するため、ストアドプロシージャは適していないし、生のSQLでやるのもミスが多そうなので避けたい。となると、Entity Frameworkを使うことになる。最新は Entity Framework Coreという名前になっているので、こちらを使った。

登録用データの作成までは良かったが、実際に動かしてみると、それなりに時間がかかった。スケジューラーに組み込んでいる他の処理との関係で、登録にかかる時間は短ければ短いほど良いので、大きく変えずに高速化する方法を調べると、公式サイトに以下のページがあった。

docs.microsoft.com

github.com

ここで紹介されているライブラリ「EFCore.BulkExtensions」を導入すると、Insert高速化 & InsertOrUpdateが出来るようになった。具体的な記録は取っていないけど10万レコードで試した所、確かに速くなっているように感じた。InsertOrUpdate機能も要件にマッチしていたので、これを採用することにした。

サイトの方に使い方がのっているが、簡単なサンプルを残しておく。削除は要件になかったので、使用せず。

// EntityクラスBookを作成(POCO)
Book book = new Book {
    // プロパティを設定
}

// Insert Or Update処理。SQLのMERGE文が呼び出される
// contextは、Entity Frameworkで作成したDbContextのサブクラス
// 1レコードでもリストを作成する
context.BulkInsertOrUpdate(new List<Book> { book });

実際に使用した時は、リストに全て含めると、内1件でエラーが発生した時にどこでエラーになったかがSQLを見てもわからないため、数件ずつのリストに分割して呼び出すようにした。

以上

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の更新と再起動を行うことが出来ました。

以上。