新しもの好きプログラマの耳より情報ブログ

仕事でもあるプログラミングについて役に立ちそうな情報を発信していこうというブログです。役に立たなそうな情報はfacebookで。

WPFのWebView2が実行環境で表示されない場合、exeがあるフォルダに実行ユーザーの書き込み権限が無いのが問題かも

概要

WPFの画面にWebView2を実装。デバッグ実行すると問題ないが、実行環境にインストールすると表示されない・・・?

そういう場合、WebView2はexeがあるフォルダへの書き込み権限を要求するという特徴のせいかもしれません。その説明と解決策です。

最初に結論まとめ

  • WebView2のインスタンスに、下記※1のように設定して、確実に書き込み権限があるフォルダを使わせるようにしましょう。
  • 処理に時間がかかること、処理が終わるまでWebView2を使用できない(Sourceを与えたりしてはいけない)という点に注意です。

※1

//xaml側でName="webView"と定義している前提です
var webViewUserDataFolder = @"<このexe専用の、ユーザーが書き込み可能なフォルダのフルパス>";
var cwv2Environment = await CoreWebView2Environment.CreateAsync(null, webViewUserDataFolder, new CoreWebView2EnvironmentOptions());
await webView.EnsureCoreWebView2Async(cwv2Environment);

説明と解決方法

実行環境にインストールすると、WebView2コントロールだけが表示されない。例外やメッセージが出たり異常終了するわけでもなく、ただ表示されないだけ。こんな現象に出会って、解析に手間取りました。

そういう場合、WebView2を実装しているexeファイルのインストール先に、実行ユーザーの書き込み権限があるかを確認しましょう。

実行はできるが書き込み権限が無いというのは、意外にあり得るケースです。例えば、インストーラで管理者権限を使ってProgram Filesへインストールしたexeを、UAC昇格せずにユーザーが実行する場合、書き込み権限が無い状態になります。

なぜその権限が必要かというと、WPF向けWebView2がデフォルトでは、実装されたexeと同じフォルダにデータファイルを作ろうとするためです。

こうした場合、exeから確実に書き込み可能なフォルダを、データファイル作成用の場所として与えてやる必要があります。例えばそのユーザーのTempフォルダ等です。

WebView2のインスタンスを生成してから、最初に次のようにすることで設定できます。この例では、Tempの下の「MyExeFolder」というこのexe固有のフォルダを与える例としました。

var webViewUserDataFolder = Environment.ExpandEnvironmentVariables(@"%Temp%\MyExeFolder");
var cwv2Environment = await CoreWebView2Environment.CreateAsync(null, webViewUserDataFolder, new CoreWebView2EnvironmentOptions());
await webView.EnsureCoreWebView2Async(cwv2Environment);

WebView2を動かしてから(例えばSourceを設定してから)変更することはできないので、処理順序に注意しましょう。WPFのウインドウに置く場合、Window.Loadedなどのタイミングで最初に処理してしまうのが良いと思います。

またawaitが入っているところからも見て取れると思いますが、処理に時間がかかるという点にも注意です。「最初はWebView2部分が非表示で、処理が終わり次第表示される」という動きで良ければ上記のように単純な呼び出しで良いですが、「表示されるまでの間はプログレスバーを出すなど動きを変える」といった場合は呼び出し箇所や順序に一工夫が必要になると思います。

まとめ

普通のコントロールと同じ感覚でWebView2をWPFの画面上で使ったら、デバッグ実行では気づけないような意外な落とし穴がありました。使う機会があったら思いだしてみてください。

.NET MAUIで普通のデスクトップアプリをビルドするには

概要

.NET MAUIで、普通のデスクトップアプリをビルドする方法の話です。普通というのは、msixインストーラで配布するパッケージではなく、exeとdllを作ってmsiインストーラなどで配布するタイプのアプリです。

先に結論まとめ

  • プロジェクトファイルに<WindowsPackageType>None</WindowsPackageType>を追加
  • launchSettings.jsonのcommandNameをProjectに
  • 発行はdotnet publishコマンドに「p:WindowsPackageType=None」を付ける
  • 以上で、普通のデスクトップアプリとしてビルド・デバッグ・発行ができます
  • ただしまだ問題点が多いようなので、実運用の開発への採用は要注意

話の背景

.NET MAUIでWindowsアプリを作ろうとすると、msixインストーラで配布するという形の、どちらかというと個人向けのアプリ開発に特化したテンプレートや説明になっています。

しかし今までWPFなどでビジネス向けアプリのGUIを作っていたという場合、普通のデスクトップアプリを作って、msiインストーラなどで配布したい、となると思います。私はそうです。

MAUIやWinUI 3ではそういうものをパッケージされていない(Unpackaged)アプリと呼ぶようです。しかしそれを作ろうとすると・・・意外なほどテンプレートも説明もまとまっていません。どちらかというと、「少しずつサポートしていく予定だが、後回し」という雰囲気です。

そのためWebの情報などを色々当たって、正しそうだと思ったやり方をまとめました。

やりかた

ビルド・デバッグ

テンプレートでプロジェクトを作成した直後は、msixを使用したストアアプリを作る設定になっています。そのためUWPなどと同様に、デバッグ実行しようとすると「開発者モードをONにしろ」と言われます。

まずはこれを、WPFなどと同様にデスクトップアプリをビルド・デバッグ実行する設定に変更します。

プロジェクトファイルを開きます。VisualStudioなら、プロジェクトファイルをダブルクリックです。 XMLProjectPropertyGroupと辿り、その中に<WindowsPackageType>None</WindowsPackageType>という要素を追加します。

次のようになります。XML全体は長いので追加分は上の方に入れて、それ以降は省略しました。

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
        <WindowsPackageType>None</WindowsPackageType>

次にソリューションエクスプローラへ戻り、プロジェクトの「Properties\launchSettings.json」ファイルを開きます。

次のようにMsixPackageを指定している部分があるので、これをProjectに変更します。

"commandName": "MsixPackage"

"commandName": "Project"

次のようになります。

{
  "profiles": {
    "Windows Machine": {
      "commandName": "Project",
      "nativeDebugging": false
    }
  }
}

以上の変更で、ビルド・デバッグはOKです。そのままビルド・デバッグ実行すると、デスクトップアプリとしてデバッグ実行されます。Windowsの開発者モードをONにする必要もありません。

発行

2024/2頃のVisualStudio 2022 Ver.17.9.1で試すと、プロジェクトファイルを上記のように変更した時点で、GUIからの「発行」が無効になります。発行はコマンドでやるしかなさそうです。

これについてはMS Docsにも情報がありました。 Use the CLI to publish unpackaged apps for Windows - .NET MAUI | Microsoft Learn

基本は次のようなコマンドになると思います。

dotnet publish -f net8.0-windows10.0.19041.0 -c Release -p:RuntimeIdentifierOverride=win10-x64 -p:WindowsPackageType=None

この記事でのポイントは -p:WindowsPackageType=None になります。プロジェクトファイルの記載と同じで、これによって普通のデスクトップアプリとして発行されます。

PublishSingleFileは未完成の様子

発行は問題なくできますが、大量のdllファイルが一緒に出来上がるので、1ファイルにまとめられるならまとめてしまいたい場合も有ると思います。この記事のように普通のデスクトップアプリにする場合でも、dotnet publishのPublishSingleFileオプションが使えます。

使えますが・・・未完成なのか、正常動作しないようです。

オプションを付けて実行すると、dllファイルはかなり減り、その分だけexeファイルのサイズが大きくなります。しかし、かなり減っただけでdllファイルはいくつか残っています。(ネイティブのdllかもしれませんが)また、実行するとエラーコード0xc000027bで異常終了します。

注意点

以上で普通のデスクトップアプリを作って動かすことは一応できました。ただ、次のissueを見た感じでは、まだまだ課題が残っていて開発途上のようです。実運用の開発に採用するにはまだ、クリティカルな問題が無いかどうかを慎重にチェックしながら使う必要がありそうです。

Windows Unpackaged support in .NET MAUI · Issue #10564 · dotnet/maui · GitHub

まとめ

.NET MAUIで普通のデスクトップアプリを作ることは、情報を集めれば一応できるようです。まだ実用には怖いところですが、WinUI 3やWPFなどの状況を考えると今後.NET MAUIがデスクトップアプリの有力な選択肢に入ってくる可能性は高いと思うので、少しずつでも調べて実戦投入に備えておくのが良いと思います。

テストツールを作るならNUnitConsoleのほうが

最初にまとめ

こんな内容の記事です。

  • ボタンかコマンドがたくさん並んだ一品もののテストアプリを、毎回一から作ってる?
  • それなら、UI部分はNUnitConsoleに任せて、ロジックだけに集中しよう!
  • その使い方を紹介

概要

DLLのようなロジックだけを開発する場合、テスト用にそれを色々なパラメータで呼び出すテストアプリを作ることが有ると思います。ボタンとコンボボックスとテキストボックスが大量に並んだテストアプリ、見たことがある人は多いんじゃないでしょうか。

こんな感じの画面

GuiSample.png

あるいはこんな感じでコマンドを打つツールです。

commandSample.png

そうしたテストツールを書くのにも、単体テストコードでお馴染みのNUnitを使うという手があります。NUnitというと開発環境の上で、VisualStudio等の開発IDEから実行するイメージが強いと思いますが・・・ 実は、テスト環境で実行することもできます。NUnitのDLLをコマンドプロンプトから実行できるNUnitConsoleを使います。

こんな構成です。

つまり、exeとしてのインターフェース部分(コマンドの解析処理など)はNUnitConsoleにお任せして、いきなりDLLを呼び出せるということです。

これを使えばテストアプリを作るときに、さっそくロジックから書けます。テストツール作成にありがちな、一品物のUI部分(CLI含む)を毎度毎度作り込むような工数の無駄が、省けるというわけです。

さらに普段から単体テストコードを書いているようなプロジェクトであれば、単体テストのコードをそのまま生かすこともできて、より効率が良くなります。 単体テスト実行の時はスタブを与え、テストツールとして実行するときは本物のインスタンスを与えれば良いわけです。

つまりこういうことです。

使ってみる

メリットを語ったところで、実際の使い方を紹介していきます。

テスト環境に入れる

NUnitConsoleをここからダウンロードして、テスト環境に持ち込みます。

https://github.com/nunit/nunit-console/releases/latest

zipとmsiインストーラ)があります。msiの方がパスも通してくれるなど楽そうですが、バージョン3.17.0を試したら.NET 8版が含まれていなかったりしたので・・・zipの方を解凍してカレントに気をつけて実行する方が良いかもしれません。以下はzip前提で書きます。

準備

まず最初に、どのバージョンの.NETで実行するかを決めます。バージョンごとにフォルダが分かれていますが、自分が作成したNUnitのDLLに合わせたものを選んでください。今回の例では.NET 8で作成したので、「bin\net8.0\nunit3-console.exe」を使用します。そのため、「bin\net8.0\」のフォルダをコマンドプロンプトのカレントにします。DLLロードの都合で同じフォルダに有った方が楽なので、テスト対象のDLLも同じフォルダへ置きます。

指定したメソッドを実行する

NUnitで作成してビルドしたDLL(ここではNUnit1.dll)を用意します。最初の例では、その中で「Test1」というメソッドを実行してみます。こんなコマンドになります。

nunit3-console.exe NUnit1.dll --where="method == Test1"

割と直観的に使えそうな感じではないでしょうか。exeの1つ目のオプションにテスト対象DLLを渡し、「--where」オプションで、実行したいメソッドの条件を指定します。

ここでは、メソッドTest1を、こんな感じでメッセージだけ出力する実装にしています。

[Test]
public void Test1()
{
    Console.WriteLine("Test1 Run");
}

すると、こんな感じで実行されます。「Test1 Run」の部分が、テストコード内で出力したメッセージです。 スクリーンショット 2024-02-15 203118.png

ここでは、「メソッド名がTest1と完全一致する」という条件にしました。--whereには、正規表現での指定や、名前のみならずAutherでのフィルタなど色々なオプションがあるので、やりたいことはだいたいできると思います。詳しくはこちらを。 https://docs.nunit.org/articles/nunit/running-tests/Test-Selection-Language.html

パラメータを変えて実行する

テストツールとして使うには、パラメータを与えて実行するという使い方も必要です。テストコードの実装側で、TestContext.Parametersでパラメータを受け取るように実装しておけば、コマンドでパラメータを与えることができます。

テストコード側の実装はこのようになります。パラメータを指定されていない場合はnullが返るので、その場合は"default"と言うパラメータで動作するように実装しています。

[Test]
public void Func2()
{
    var param1 = TestContext.Parameters["param1"] ?? "default";
    Console.WriteLine($"Func2 Param {param1}");
}

このコードを次のコマンドで実行してみます。パラメータ「param1」に値「value1」を与えています。

nunit3-console.exe NUnit1.dll --where="method == Func2" --testparam:param1=value1

すると、このようになります。「Func2 Param value1」と出ているので、"default"ではないパラメータをコマンドから与えられたことが分かります。

スクリーンショット 2024-02-24 150009.png

まとめ

NUnitの使い方さえ理解してしまえば、NUnitConsoleを使った方が、いちいちテストのためだけのアプリを1から作るより楽ではないでしょうか?NUnitは初めてなので学習が必要だという場合でも、UnitTestのコードを気軽に書けるように慣れておくことは、無駄にはならないと思います。次にテストアプリを書こうと思った時には、ぜひ使ってみてください。

VRもくもく会を今週もやりました&その様子の写真

VRもくもく会を今週も開催しました。これは、「VRでのイベントというのも今後は選択肢に入ってくるのではないか、とにかくやってみて経験値を溜めてみよう」ということでやっています。2~3週間おきの定期開催ということで、もう1年以上続けています。

・・・定期開催なので、他のブログ記事も書かないと、記事がこれで埋まってしまいますね。他も書くように頑張ってみます。

今日は成果発表コーナーで、こんな発表がありました。

  • mishizakiさん:.NET 9をインストールして、プロジェクトを作ろうとしてみた。まだチャレンジ中。
  • 私(suusanex):「リアルで人が集まって朝礼みたいな軽いミーティングをするときに、一部の人だけオンライン参加」というようなハイブリッド局面に対応できるハードがない。ミーティング用設置型マイクスピーカーのようなものは、周りの音も拾うのでこの用途に合わない。そういう時に使えるハード構成案を書いてみた。提案してみる予定。

成果発表コーナーの様子はこんな感じです。写真を貼っておきます。

発表コーナー1

発表コーナー2

VRもくもく会を今週もやりました&その様子の写真

VRもくもく会を今週も開催しました。これは、「VRでのイベントというのも今後は選択肢に入ってくるのではないか、とにかくやってみて経験値を溜めてみよう」ということでやっています。2~3週間おきの定期開催ということで、もう1年以上続けています。

前回までは動画配信のチャレンジもしていましたが、今回は普通にもくもく会です。

今日は成果発表コーナーで、こんな発表がありました。

  • mishizakiさん:プログラムを久々に書いてみた。Surfaceでも行けるかと思ったら、色々辛かった。キーボードとかいろいろ小さいし、GitHub Copilotの提案が画面を埋め尽くしたりする。
  • 私(suusanex):ブログのネタはたまるが一向に書けてないので、記事1つを途中まで書いてみた。調べ物が必要になったので、今回は途中まで。

成果発表コーナーの様子はこんな感じです。写真を貼っておきます。

発表コーナー1

発表コーナー2

VRもくもく会を今週もやりました&その様子の写真

VRもくもく会を今週も開催しました。これは、「VRでのイベントというのも今後は選択肢に入ってくるのではないか、とにかくやってみて経験値を溜めてみよう」ということでやっています。2~3週間おきの定期開催ということで、もう1年以上続けています。

今日は成果発表コーナーで、こんな発表がありました。

  1. mishizakiさん:Azure Developer CLI をインストールしてみた
  2. 私(suusanex):wixsharpを試してみたら不便な点もあるけどWPF+C#インストーラ作れるのは魅力

成果発表コーナーの様子はこんな感じです。写真を貼っておきます。

成果発表の様子
成果発表の様子2

C#でのファイルパスの重複排除(大小文字無視)は、Dictionaryなどのコレクションにやらせると便利

概要

Windowsのファイルパスなどで、大文字小文字を無視して文字列比較をしたい場合があります。C#では、単純な比較もできますし、ソートと一致判定を伴うコレクション(HashSet,Dictionaryなど)でもそうした比較を行うことができます。ただし比較方式にはいくつかの選択肢があります。これらについて、意外とちゃんと使えていないコードを見かけるので、小ネタですが記事を書きました。

結論

最初に結果だけ言うと、このように書けば、大文字小文字を無視して比較し重複排除するリストができます。これを見て「常識だろ」と思う人も多いと思いますが、意外に「何それ?!」という人もいます。

HashSet<string> filePaths = new(StringComparer.OrdinalIgnoreCase);
var add = @"C:\FilePath1"; //追加するファイルパス
filePaths.Add(add);

よく見るコードとその改善

使えていないというのは、.NETがやってくれることを次の例のように自力でやってしまうということです。意外に、次のようなコードはよく見かけます。(そういうコードではLINQも使っていないことが多いですが、この話と無関係なので使うサンプルにします)

使えていない例

List<string> filePaths = new();
var add = @"C:\FilePath1"; //追加するファイルパス
if (filePaths.All(d => d.ToUpper() != add.ToUpper()))
{
    filePaths.Add(add);
}

大小文字無視の比較のためにToUpper()を使い、既存のListに含まれていない場合はListへ追加しています。

もちろんこのコードでも動くとは思いますが、次のように.NETにやってもらった方が保守性も、おそらくは性能も上がります。

改善1:大小文字無視の比較はEqualsメソッドで

ToUpper()で頑張らなくても、文字列のEqualsメソッドは、大小文字無視の比較ができます。

List<string> filePaths = new();
var add = @"C:\FilePath1"; //追加するファイルパス
if (filePaths.All(d => d.Equals(add, StringComparison.OrdinalIgnoreCase)))
{
    filePaths.Add(add);
}

改善2:コレクションの重複は、コレクションに判定させる

重複排除したリストを作りたいなら、HashSetが使えます。HashSetは、コンストラクタに指定することで大小文字無視の比較ができます。

HashSet<string> filePaths = new(StringComparer.OrdinalIgnoreCase);
var add = @"C:\FilePath1"; //追加するファイルパス
filePaths.Add(add);

Dictionaryでも使えます

Dictionaryのキーの判定にも同じ方法を使えます。それ以外にもConcurrentDictionary等のソート・一致判定を伴うコレクションなら、おそらく全て使えます。

Dictionary<string, int> filePathAndValues = new(StringComparer.OrdinalIgnoreCase);
//追加するファイルパスと値
var addFilePath = @"C:\FilePath1";
int addValue = 1;
filePathAndValues.Add(addFilePath, addValue);

StringComparerの選び方

StringComparerというのが出てきましたが、大小文字無視(IgnoreCase)のものだけでも3種類あります。どれを使ったら良いでしょう。これも割と、書く人によってバラバラだったりします。

Windowsのファイルパスの判定が目的なら、StringComparer.OrdinalIgnoreCaseを使いましょう。大文字小文字の差を無視した上でのシンプルなバイナリ比較なので、ファイルパスのような一致判定には適しています。

悩む人が多かったのか、Learnにまとまっていました。ここでもやはり、ファイルパスなどはOrdinalIgnoreCaseが推奨されています。

.NET での文字列の比較に関するベスト プラクティス - .NET | Microsoft Learn

まとめ

単純なファイルパスの一致判定と重複排除だけの話ですが、ちゃんと.NETのライブラリを使うかどうかによって、コードの見やすさもおそらくは性能もだいぶ変わって来ます。こうしたことの積み重ねが、意外と保守性や性能に効いてくることもあると思うので、知らなかった人はぜひ使ってみてください。知っていた人は、周りに分かっていない人がいたらぜひ教えてあげて欲しいなと思います。