WPF の起動時をカスタマイズする
◆ Main 関数を書けるように
◆ app.Startup
◆ 多重起動禁止
◆ mutex/semaphore
◆ グローバル例外のハンドル
◆ app.Startup
◆ 多重起動禁止
◆ mutex/semaphore
◆ グローバル例外のハンドル
メインウィンドウを開くまで
デフォルトだと App.xaml の StartupUri 属性に指定された Window が開かれますWindow を起動する前の処理を書いたり 開き方を制御するには この設定を解除します
まず App.xaml のビルドアクションを Application Definition から Page にします
ソリューションエクスプローラで App.xaml を選んで プロパティの ビルドアクションを選択すればできます
次に App.xaml.cs に Main 関数を作ります
デフォルトは空なので こんな感じにします
public partial class App : Application
{
[System.STAThreadAttribute()]
static public void Main()
{
var app = new App();
app.InitializeComponent();
app.Startup += App_Startup;
app.Run();
}
private static void App_Startup(object sender, StartupEventArgs e)
{
new MainWindow().Show();
}
}
{
[System.STAThreadAttribute()]
static public void Main()
{
var app = new App();
app.InitializeComponent();
app.Startup += App_Startup;
app.Run();
}
private static void App_Startup(object sender, StartupEventArgs e)
{
new MainWindow().Show();
}
}
App_Startup では好きな方法で Window を開けます
if (debug_mode)
{
new DebugManagerWindow().Show();
}
else
{
SoloWindow.Show(typeof(MainWindow));
}
{
new DebugManagerWindow().Show();
}
else
{
SoloWindow.Show(typeof(MainWindow));
}
こんな風に デバッグ用なら別のウィンドウを最初に開いたり 同時に 1 つしか開かないような制御のための特殊な開き方もできます
注: DebugManagerWindow や SoloWindow クラスは .NET に用意されてるものではないです
他には起動時に 3 つウィンドウを開くなどもできます
最初に複数ウィンドウを開きたいけど MainWindow が他を開く作りにしたくないときに使えます
Window の表示がいらない と言う場合もあります
例えば Main 関数の最初にコマンドライン引数を見て --version があれば app のインスタンスすら作らずにバージョンだけ表示して終わることもできます
var args = Environment.GetCommandLineArgs();
if (args.Contains("--version"))
{
MessageBox.Show("1.2.3.4", "Version");
return;
}
if (args.Contains("--version"))
{
MessageBox.Show("1.2.3.4", "Version");
return;
}
多重起動禁止
けっこうやりたい需要ありそうなのに簡単にはできずちょっと工夫が必要でプロセス制御系の機能を使います調べてみても方法は mutex か semaphore の 2 択みたい
semaphore のほうが新しい機能で mutex は 「同時に 1 つ」 だったのが semaphore では「同時にN個まで」と指定もできるようです
新しいと言っても .NET 2.0 でそれ以降は多重起動禁止できそうな仕組みはなさそう
App.xaml の属性に allowMultiInstance="false" でできればいいのになぁとよく思います
mutex も semaphore も OS 系で耳にする単語で排他制御の何かだったような記憶はありますがいまいち詳しいことは知らないです
ここによると WPF では 取得と解放を別スレッドでもできる semaphore のほうが向いてるらしいです
mutex だと取得したスレッドで解放しないといけないみたい
それぞれのサンプルです
ほぼ stackoverflow などのサンプルそのままで基本アプリ名だけ変えて使いまわしてます
mutex
var mutex = new System.Threading.Mutex(false, "ApplicationName");
try
{
if (mutex.WaitOne(0, false) == false)
{
MessageBox.Show("すでに実行中です。同時に1つしか起動できません。", "エラー");
return;
}
start();
}
catch (AbandonedMutexException)
{
start();
}
finally
{
mutex.ReleaseMutex();
}
try
{
if (mutex.WaitOne(0, false) == false)
{
MessageBox.Show("すでに実行中です。同時に1つしか起動できません。", "エラー");
return;
}
start();
}
catch (AbandonedMutexException)
{
start();
}
finally
{
mutex.ReleaseMutex();
}
start() メソッドでメインの処理を行います
正常でも例外でも終了した時に finally で ReleaseMutex が実行されて解放されます
AbandonedMutexException は mutex を取得したけど それが 前に別のスレッドがちゃんと mutex 解放せずに終了したせいで解放されなかったものだったときに起きる例外なので 取得はできてるから気にせず開始します
タスクマネージャからプロセス止めたり VisualStudio のデバッグ実行を停止ボタンで止めたりすると 終了処理なしで強制的に止まるのでこの例外起きるかも
semaphore
using (var semaphore = new System.Threading.Semaphore(1, 1, "ApplicationName", out var created_new))
{
if (!created_new)
{
MessageBox.Show("すでに実行中です。同時に1つしか起動できません。", "エラー");
return;
}
start();
}
{
if (!created_new)
{
MessageBox.Show("すでに実行中です。同時に1つしか起動できません。", "エラー");
return;
}
start();
}
こっちはすごくシンプルです
新しい分 IDisposable 対応で using するだけで解放できます
created_new はセマフォが作成されると true すでにあると false なので false のときが実行中ということになります
using なので自然と同じスレッドで取得解放できるようにみえるので 「WPF では取得・解放が別スレッドでもいい semaphore がオススメ」の理由はわからなかったです
semaphore って
少し気になったので調べてみると 最初の 1, 1 のところが 初期値と最大値を表すもので semaphore インスタンスの WaitOne / Release メソッドで取得・解放をするようですOS 全体ではなくて 1 つの semaphore オブジェクトが管理してるレベルのものです
WaitOne で取得しても何かのインスタンスがもらえるわけでもなく 単純に残り数をカウントを管理してるだけです
0, 3 を指定すると最大 3 つで デフォルトで 3 つとも取得されていて残りが 0
WaitOne してもどこかで Release されるまで待機することになります
3, 3 を指定すると最大 3 つで デフォルトで 3 つ残っているので WaitOne ですぐに獲得できます
読みやすくわかりやすい説明がなかったのですが ここはわかりやすかったです
http://dotnetpattern.com/threading-semaphore
英語なので軽く流し読みですが それでも納得できました
これでどうして 同時起動が防げるんだろう? と思ったのですけど 名前付き semaphore はシステム(Windows 全体)で共有でされているようです
VisualStudio の説明では createdNew はこうなってます
このメソッドから制御が戻るときに、ローカル セマフォが作成された場合 (name が null または空の文字列の場合)、または指定した名前付きシステム
セマフォが作成された場合は true が格納されます。指定した名前付きシステム セマフォが既に存在する場合は false が格納されます。このパラメーターは初期化せずに渡されます。
グローバルで共有される同じ名前がすでにあるか を見ています
semaphore インスタンス自体は使ってないのでカウントはエラーにならないならなんでもよさそうです
共有するならインスタンス作るときに最大と初期のカウントが違うとどうなるのかが気になるのですが 同じ名前でカウントが違うのをいくつか作ってみてもエラーにはならなかったです
この辺はやっぱりよくわからないです
グローバルのエラーハンドル
例外のキャッチ漏れで一々アプリが落ちると面倒です特にデバッグモードじゃない時には原因もわからなくて困ります
なのでグローバルで例外をハンドルできるようにしておくと便利です
ここでログファイルに例外メッセージを出力するのが一般的かと思います
簡単な例
var app = new App();
app.InitializeComponent();
app.DispatcherUnhandledException += (o, e) =>
{
MessageBox.Show("Catch app unhandled exception.\\n" + e.Exception.Message);
};
AppDomain.CurrentDomain.UnhandledException += (o, e) =>
{
var unhandled = (Exception)e.ExceptionObject;
MessageBox.Show("Catch appdomain unhandled exception.\\n" + unhandled.Message);
};
app.Run();
app.InitializeComponent();
app.DispatcherUnhandledException += (o, e) =>
{
MessageBox.Show("Catch app unhandled exception.\\n" + e.Exception.Message);
};
AppDomain.CurrentDomain.UnhandledException += (o, e) =>
{
var unhandled = (Exception)e.ExceptionObject;
MessageBox.Show("Catch appdomain unhandled exception.\\n" + unhandled.Message);
};
app.Run();
app の DispatcherUnhandledException と AppDomain の UnhandledException イベントがあります
DispatcherUnhandledException では名前通り UI スレッドのものを受け取れます
AppDomain の方ではすべてのハンドルされなかった例外を受け取れます
UI スレッドのものだと まず DispatcherUnhandledException のハンドラが呼び出されます
そこで e.Handled が true にされればそこで終わり されなかった場合は AppDomain のほうも呼び出されます
AppDomain の方が呼び出されると復帰ができずにあとは終了するだけです
終了する直前にログに出力などはできるので完全にその用途のものかと思います
一応本当に最初の頃 .NET 1.1 では復帰できたようで App.config に
<configuration>
<runtime>
<legacyUnhandledExceptionPolicy enabled="1" />
</runtime>
</configuration>
<runtime>
<legacyUnhandledExceptionPolicy enabled="1" />
</runtime>
</configuration>
を追加すれば昔の動きに出来るようです
ただ非推奨だそうです
サンプルにいくつか例外出すボタンを作ってみました
private void Button1_Click(object sender, RoutedEventArgs e)
{
throw new Exception("普通に例外");
}
private void Button2_Click(object sender, RoutedEventArgs e)
{
var thread = new Thread(new ThreadStart(() =>{
throw new Exception("別スレ例外");
}));
thread.Start();
}
private void Button3_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
throw new Exception("await task例外");
});
}
private void Button4_Click(object sender, RoutedEventArgs e)
{
Task.Run(() =>
{
throw new Exception("task例外");
});
}
{
throw new Exception("普通に例外");
}
private void Button2_Click(object sender, RoutedEventArgs e)
{
var thread = new Thread(new ThreadStart(() =>{
throw new Exception("別スレ例外");
}));
thread.Start();
}
private void Button3_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
throw new Exception("await task例外");
});
}
private void Button4_Click(object sender, RoutedEventArgs e)
{
Task.Run(() =>
{
throw new Exception("task例外");
});
}
結果はこうです
DispatcherUnhandledException | AppDomain | |
button1 | ○ | ○ |
button2 | × | ○ |
button3 | ○ | ○ |
button4 | × | × |
button2 の別スレッドだと DispatcherUnhandledException ではハンドルされずに AppDomain の方だけ呼び出されてます
Task だと 単に実行した button4 だと例外が起きても自動でキャッチされてどっちのハンドラも呼び出されません
button3 で Task を await すると 例外は button3 のハンドラ内の UI スレッドでの例外になるので DispatcherUnhandledException でもハンドルされるようになります
Thread 使わず Task だけなら設定するのは DispatcherUnhandledException だけでいいかも