【Windows/C#】フォームなしのアプリケーションをなるべく綺麗に書いてみる

今さらながらQiitaに登録したので、お試し投稿。
こんなので良いのだろうか・・・？

## はじめに
C#にてバックグラウンドで動作する常駐アプリケーションを開発する際に、Program.csのMainメソッド内に[DoEvents](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.application.doevents?view=netframework-4.7.2)と[Sleep](https://docs.microsoft.com/ja-jp/dotnet/api/system.threading.thread.sleep?view=netframework-4.7.2)でメインループさせるコードをよく目にするけど、もう少し綺麗に書きたい。
もっと言えば、Program.csの中身は・・・

```cs:Program.cs
/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main()
{
	Application.EnableVisualStyles();
	Application.SetCompatibleTextRenderingDefault(false);
	Application.Run(new Form1());
}
```

この形を維持して、メッセージループは標準のものを利用したい。
環境：Visual Studio 2017 / .Net 4.6 / C# 7.3

# こんなクラスをこさえてみた
Application.RunメソッドにFormクラス以外で渡せる[ApplicationContext](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.applicationcontext?view=netframework-4.7.2)クラスを使って、便利なクラスを作ってみた。
FormlessApplicationクラス: 実際に行う処理を実装する基底クラス
FormlessApplicationContextクラス: FormlessApplicationクラスを制御するクラス

```cs:FormlessApplication.cs
/// <summary>
/// メインフォームを利用しないアプリケーションの処理を実装します。
/// </summary>
public abstract class FormlessApplication : IDisposable
{
	/// <summary>
	/// このアプリケーションを実行しているコンテキストを取得します。
	/// </summary>
	protected internal FormlessApplicationContext Context { get; internal set; }

	/// <summary>
	/// アプリケーションを初期化します。
	/// </summary>
	/// <returns>処理を継続する場合は、true。それ以外は、false。</returns>
	public virtual bool Initialize()
	{
		return true;
	}

	/// <summary>
	/// アプリケーションの処理を実行します。
	/// </summary>
	public virtual void DoWork()
	{
	}

	/// <summary>
	/// 全てのリソースを破棄します。
	/// </summary>
	public void Dispose()
	{
		Dispose(true);
	}

	/// <summary>
	/// 全てのリソースを破棄します。
	/// </summary>
	/// <param name="disposing">マネージドリソースを破棄する場合は、true。それ以外は、false。</param>
	protected virtual void Dispose(bool disposing)
	{
	}

	/// <summary>
	/// アプリケーションを終了します。
	/// </summary>
	protected void ExitApp()
	{
		this.Context.ExitThread();
	}
}
```

```cs:FormlessApplicationContext.cs
/// <summary>
/// メインフォームを利用しないアプリケーションの終了を定義します。
/// </summary>
public class FormlessApplicationContext : ApplicationContext
{
	static class SafeNativeMethods
	{
		[StructLayout(LayoutKind.Sequential)]
		internal struct POINT
		{
			public int x;
			public int y;
		}

		[StructLayout(LayoutKind.Sequential)]
		internal struct MSG
		{
			public IntPtr hwnd;
			public int message;
			public IntPtr wParam;
			public IntPtr lParam;
			public int time;
			public POINT pt;
		}

		[DllImport("user32.dll", SetLastError = true)]
		[return: MarshalAs(UnmanagedType.Bool)]
		internal static extern bool PeekMessage(
			out MSG msg, IntPtr hWnd,
			int wMsgFilterMin, int wMsgFilterMax, int wRemoveMsg);
	}

	/// <summary>
	/// 実行しているアプリケーションを取得します。
	/// </summary>
	public FormlessApplication App { get; }

	/// <summary>
	/// FormlessApplicationContext クラスを初期化し、
	/// 実行するアプリケーションを設定します。。
	/// </summary>
	/// <param name="app">実行するアプリケーション</param>
	public FormlessApplicationContext(FormlessApplication app)
		: base()
	{
		this.App = app;
		this.App.Context = this;

		// アプリケーション初期化
		if (!this.App.Initialize())
		{
			// アプリケーション終了
			Application.Idle += (e, sender) => this.ExitThread();
			return;
		}

		// モーダル状態を捕捉
		Application.EnterThreadModal += this.Application_EnterThreadModal;
		Application.LeaveThreadModal += this.Application_LeaveThreadModal;
		// Idleイベント登録
		Application.Idle += this.Application_Idle;
	}

	protected override void Dispose(bool disposing)
	{
		if (disposing)
		{
			// App破棄
			this.App.Dispose();
		}

		base.Dispose(disposing);
	}

	private void Application_EnterThreadModal(object sender, EventArgs e)
	{
		// Idleイベント一時解除
		Application.Idle -= this.Application_Idle;
	}

	private void Application_LeaveThreadModal(object sender, EventArgs e)
	{
		// Idleイベント再登録
		Application.Idle += this.Application_Idle;
	}

	private void Application_Idle(object sender, EventArgs e)
	{
		// ウィンドウメッセージを受信するまでループ処理
		while (!SafeNativeMethods.PeekMessage(
			out SafeNativeMethods.MSG _, IntPtr.Zero, 0, 0, 0))
		{
			// アプリケーションの処理を実行
			this.App.DoWork();
			System.Threading.Thread.Sleep(1);
		}
	}
}
```

# 使い方
FormlessApplicationクラスを継承して、必要な処理を書くだけ。

```cs:TestFormlessApplication.cs
class TestFormlessApplication : FormlessApplication
{
	/// <summary>
	/// アプリケーションを初期化します。
	/// </summary>
	public override bool Initialize()
	{
		// 初期化処理を書く
		// エラーなどで終了したい場合は、return false;
	}

	/// <summary>
	/// アプリケーションの処理を実行します。
	/// </summary>
	public override void DoWork()
	{
		// メイン処理を書く
		// アプリケーションを終了するときは、this.ExitApp();
	}

	/// <summary>
	/// 全てのリソースを破棄します。
	/// </summary>
	/// <param name="disposing">マネージドリソースを破棄する場合は、true。それ以外は、false。</param>
	protected override void Dispose(bool disposing)
	{
		// リソースの解放処理を書く

		base.Dispose(disposing);
	}
}
```

Program.csの中身はシンプルにApplication.Runの引数を変えるだけ。

```cs:Program.cs
/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main()
{
	Application.EnableVisualStyles();
	Application.SetCompatibleTextRenderingDefault(false);
	Application.Run(new FormlessApplicationContext(new TestFormlessApplication()));
}
```

これなら標準の処理を極力活かしつつループ処理ができる。

# ポイント
### 初期化
```cs:FormlessApplicationContext.cs
// アプリケーション初期化
if (!this.App.Initialize())
{
	// アプリケーション終了
	Application.Idle += (e, sender) => this.ExitThread();
	return;
}
```

FormlessApplicationクラスのコンストラクタで初期化処理を行うと、エラーなどで終了したいときに困るので、初期化関数を別で用意した。（上手くやればコンストラクタで初期化する形でもいける気がする）
またコンストラクタ内でExitThread()を呼んでも終了できないようなので、Idleイベントで呼ばれるように工夫。

### モーダルの罠
```cs:FormlessApplicationContext.cs
// モーダル状態を捕捉
Application.EnterThreadModal += this.Application_EnterThreadModal;
Application.LeaveThreadModal += this.Application_LeaveThreadModal;
```

ループ処理中にダイアログをShowDialogすると、更にIdleイベントが呼ばれてダイアログが無限に表示される現象が発生。
モーダル状態に入ったときはIdleイベントを一時的に解除することで回避。

### ループ処理
```cs:FormlessApplicationContext.cs
private void Application_Idle(object sender, EventArgs e)
{
	// ウィンドウメッセージを受信するまでループ処理
	while (!SafeNativeMethods.PeekMessage(
		out SafeNativeMethods.MSG _, IntPtr.Zero, 0, 0, 0))
	{
		// アプリケーションの処理を実行
		this.App.DoWork();
		System.Threading.Thread.Sleep(1);
	}
}
```
溜まったメッセージを処理し終わったときにIdleイベントが「１度だけ」発生する。逆に言うとメッセージが溜まって処理されないとIdleイベントが発生しない。

それを利用して、起動時のメッセージ処理後のIdleイベントでループ処理を行い、メッセージが来たら処理を返してあげて、そのメッセージ処理後のIdleイベントで再びループ処理を行い、メッセージが来たら・・・ｒｙ（無限ループ）
メッセージが溜まってるかどうかを判断するためにWinAPIのPeekMessage関数を使用。（メッセージの処理自体は標準の処理にお任せ）

#さいごに
メインループほどの頻度が必要なければ、TimerとNotifyIconを利用するようなComponentクラスを作って、ApplicationContextクラスから使えば、WinAPIやIdleイベントを使うことなく、お手軽に通知領域に表示する常駐アプリが作れたりもする。