2019 年 5 月

Volume 34 Number 5

[.NET Core 3.0]

.NET Core 3.0 の WinForms で集中管理された pull request ハブを作成する

Eric Fleming Fleming | 2019 年 5 月

Windows フォーム (またの名は WinForm) は、リッチな対話型インターフェイスを持つ強力な Windows ベースのアプリケーションを開発するために長年使用されています。あらゆる種類の企業でこれらのデスクトップ アプリケーションに幅広く投資がなされており、240 万人に及ぶ開発者が Visual Studio を使用して毎月デスクトップ スタイルのアプリを作成しています。既存の WinForms コード資産を活用し、拡張できるという利点は魅力的ですが、他にも利点があります。WinForms のドラッグ アンド ドロップのデザイナー エクスペリエンスにより、ユーザーは、特別な知識やトレーニングなしで完全に機能する UI を構築できます。WinForms アプリケーションは、簡単に配置および更新でき、インターネット接続から独立して作業でき、インターネットに構成を公開しないローカル コンピューターで実行することによりセキュリティを強化できます。最近まで、WinForms アプリケーションは完全な .NET Framework を使用してのみ構築できましたが、.NET Core 3.0 プレビューのリリースにより状況が変化しました。

.NET Core の新機能と利点は、Web 開発をしのいでします。.NET Core 3.0 を使用すると、より簡単な配置、パフォーマンスの向上、.NET Core 固有の NuGet パッケージのサポート、.NET Core コマンド ライン インターフェイス (CLI) などの機能が WinForms に追加されます。この記事では、これらの利点の多くを取り上げて、それらが重要である理由と、WinForms アプリケーションで使用する方法について説明します。

では、さっそく初めての .NET Core 3.0 WinForms アプリケーションを構築してみましょう。この記事では、GitHub でホストされているオープンソースの Microsoft リポジトリの 1 つに対してオープンな pull request を取得して表示するアプリケーションを構築します。最初のステップとして、Visual Studio 2019 と.NET Core 3.0 SDK の最新バージョンをインストールします。すると、.NET Core CLI コマンドにアクセスできるようになり、新しい WinForms アプリケーションを作成できます。これは、.NET Core サポートが追加される前の WinForms アプリケーションではできないことでした。

.NET Core 3.0 を対象とする WinForms プロジェクトを作成できる新しい Visual Studio テンプレートは、近日公開予定です。これはまだ使用できないため、ここでは次のコマンドを実行して PullRequestHub という名前の新しい WinForms プロジェクトを生成しましょう。

dotnet new winforms -o PullRequestHub

プロジェクトが正常に作成されたことを確認するために、dotnet new コマンドによって作成された新しいディレクトリに移動し、CLI を使用して次のようにプロジェクトをビルドおよび実行します。

cd .\PullRequestHub\

.NET Core CLI にアクセスできるため、復元、実行、ビルドを行うコマンドにもアクセスできます。実行の前に、次のようにして復元およびビルドのコマンドを試してみましょう。

dotnet restore
dotnet build

これらのコマンドは、.NET Core Web アプリケーションのコマンド ラインで実行している場合と同様に動作します。なお、dotnet run コマンドを実行すると、実際には、アプリが実行される前に復元とビルドの両方が実行されることにご注意ください (bit.ly/2UCkEaN)。では、プロジェクトを実行してテストするために、コマンド ラインに dotnet run を入力してみましょう。

サインインが完了しました!初めての .NET Core WinForms アプリケーションは、先ほど作成しました。実行すると、"Hello .NET Core!" というテキストを含んだフォームが画面に表示されます。

話をさらに進めてアプリケーションにロジックを追加する前に、Visual Studio の WinForms デザイナー ビューの現在の状態について少しご説明しましょう。

.NET Core WinForms アプリ用のデザイナーの設定

CLI で生成したプロジェクトを Visual Studio で開くと、一部の機能がないことに気づく場合があります。最も顕著なのは、.NET Core WinForms アプリケーション用のデザイナー ビューが提供されていないことです。この機能を利用可能にする計画はあるのですが、まだ完成していません。

幸い、これには回避策があり、少なくともネイティブ サポートが追加されるまでの間デザイナーにアクセスすることができます。今のところ、UI ファイルを含む .NET Framework プロジェクトを作成するという方法があります。こうすれば、デザイナーを使用して UI ファイルを編集することができ、.NET Core プロジェクトでは .NET Framework プロジェクトから UI ファイルが参照されます。これにより、.NET Core でアプリケーションを構築中に UI 機能を活用することができます。私のプロジェクトでこれを行う方法をご紹介しましょう。

作成した PullRequestHub プロジェクトに、.NET Framework の完全バージョンで実行する新しい WinForms プロジェクトを追加します。このプロジェクトに PullRequestHub.Designer という名前を付けます。新しいプロジェクトを作成した後、.NET Core プロジェクトから Form1 ファイルを削除して、Program.cs クラスのみを残します。

PullRequestHub.Designer に移動し、フォーム ファイルの名前を PullRequestForm に変更します。次に、.NET Core のプロジェクト ファイルを編集し、両方のプロジェクトのファイルをリンクするために次のコードを追加します。これにより、今後追加するフォームやリソースも適切に処理されます。

<ItemGroup>
  <Compile Include=”..\PullRequestHub.Designer\**\*.cs” />
</ItemGroup>

プロジェクト ファイルを保存すると、PullRequestForm ファイルがソリューション エクスプローラーに表示され、それらのファイルを操作できるようになります。UI エディターを使用するには、.NET Core プロジェクトから PullRequestForm ファイルを閉じた後、.NET Framework プロジェクトから PullRequestForm ファイルを開く必要があります。変更は両方のプロジェクトに加えられますが、エディターは .NET Framework プロジェクトからのみ使用可能です。

アプリケーションのビルド

アプリケーションにコードを追加してみましょう。GitHub からオープンな pull request を取得するには、HttpClient を作成する必要があります。ここで、新しい HttpClientFactory へのアクセスを提供する .NET Core 3.0 の出番です。完全フレームワーク バージョンの HttpClient にはいくつかの問題点があり、その 1 つは using ステートメントを使用してクライアントを作成する場合の問題です。HttpClient オブジェクトは破棄されますが、基になるソケットは少し時間 (既定では 240 秒) がたってから解放されます。ソケット接続が 240 秒のあいだ開いたままになると、高スループットのシステムで空きソケットが飽和状態になる可能性があります。その状態になると、新しい要求が空きソケットを待機する必要が生じ、パフォーマンスへのさらに重大な影響が発生することがあります。

HttpClientFactory は、これらの問題の軽減に役立ちます。1 つには、より中央に近い場所にクライアントの実装を事前構成する簡単な方法が提供されます。また、HttpClient の有効期間が自動的に管理されるため、前述の問題が発生しなくなります。WinForms アプリケーションでこれをどのように行えるようになったかを見てみましょう。

この新しい機能を使用する最適で最も簡単な方法の 1 つは、依存関係の挿入を使用することです。依存関係の挿入 (より一般的には制御の反転) は、依存関係をクラスに渡すための手法です。これは、クラスの結合を削減し、単体テストを容易にするための優れた手段でもあります。たとえば、プログラムのスタートアップ中に IHttpClientFactory のインスタンスを作成し、後でそのオブジェクトをフォームで利用できます。これは、以前のバージョンの .NET で WinForms を使用する場合は簡単には実行できず、.NET Core を使用するもう 1 つの利点となっています。

これから、Program.cs で ConfigureServices という名前のメソッドを作成します。このメソッドでは、依存関係の挿入を使用してサービスを利用可能にするために新しい ServiceCollection を作成します。まず、次の 2 つの NuGet パッケージについて最新版をインストールする必要があります。

  • 'Microsoft.Extensions.DependencyInjection'
  • 'Microsoft.Extensions.Http'

その後、図 1 に示すコードを追加します。これにより、フォームで使用する新しい IHttpClientFactory が作成されます。結果は、GitHub の API に関連する要求のために明示的に使用できるクライアントです。

図 1 新しい IHttpClientFactory の作成

private static void ConfigureServices()
{
  var services = new ServiceCollection();
  services.AddHttpClient();
  services.AddHttpClient(“github”, c =>
  {
    c.BaseAddress = new Uri(“https://api.github.com/”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/vnd.github.v3+json”);
    c.DefaultRequestHeaders.Add(“User-Agent”, “HttpClientFactory-Sample”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/json”);
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
  });
}

次に、実際のフォーム クラス PullRequestForm を、シングルトンとして登録する必要があります。このメソッドの最後に、次の行を追加します。

services.AddSingleton<PullRequestForm>();

次に、ServiceProvider のインスタンスを作成する必要があります。Program.cs クラスの先頭で、次のプロパティを作成します。

private static IServiceProvider ServiceProvider { get; set; }

これで ServiceProvider のプロパティを用意できましたので、ConfigureServices メソッドの最後に、次のように ServiceProvider を作成する行を追加します。

ServiceProvider = services.BuildServiceProvider();

すべてを終えると、完全な ConfigureServices メソッドは図 2 のコードのようになります。

図 2 ConfigureServices メソッド

private static void ConfigureServices()
{
  var services = new ServiceCollection();
  services.AddHttpClient();
  services.AddHttpClient(“github”, c =>
  {
    c.BaseAddress = new Uri(“https://api.github.com/”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/vnd.github.v3+json”);
    c.DefaultRequestHeaders.Add(“User-Agent”, “HttpClientFactory-Sample”);
    c.DefaultRequestHeaders.Add(“Accept”, “application/json”);
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
  });
  services.AddSingleton<PullRequestForm>();
  ServiceProvider = services.BuildServiceProvider();
}

フォームには、開始時にコンテナーを関連付ける必要があります。そうすれば、アプリケーションを実行したときに、必要なサービスを利用可能な状態で PullRequestForm が呼び出されます。Main メソッドを次のコードに変更します。

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  ConfigureServices();
  Application.Run((PullRequestForm)ServiceProvider.GetService(typeof(PullRequestForm)));
}

すばらしいですね。これで関連付けの作業は完了しました。PullRequestForm のコンストラクターでは、次のコードに示すように、関連付けを行った IHttpClientFactory を挿入してローカル変数に割り当てます。

private static HttpClient _httpClient;
public PullRequestForm(IHttpClientFactory httpClientFactory)
{
  InitializeComponent();
  _httpClient = httpClientFactory.CreateClient(“github”);
}

これで、pull request やイシューなどを取得するための GitHub 呼び出しで使用する HttpClient を用意できました。この後のステップは少し複雑です。HttpClient からの呼び出しは非同期の要求になります。WinForms を使用したことがあれば、次に何が起こるかおわかりでしょう。スレッドを処理して、UI スレッドにディスパッチ更新を送信する必要があります。

すべての pull request の取得を開始するために、ビューにボタンを追加しましょう。こうしておけば、今後、確認するリポジトリやリポジトリ グループを追加できます。関連付けたデザイナーを使用して、フォームにボタンをドラッグし、テキストを "Microsoft" に変更します。 また、ボタンに RetrieveData_Button などの意味のある名前を付けます。RetrieveData_Button_Click イベントを関連付ける必要がありますが、非同期にする必要があるため、次のコードを使用します。

private async void RetrieveData_Button_Click(object sender, EventArgs e)
{
}

オープンな GitHub pull request を取得するメソッドは、この場所で呼び出すことになります。ただし、非同期の呼び出しであるため、まず SynchronizationContext を関連付ける必要があります。それには、新しいプロパティを追加し、コンストラクターを次のコードに更新します。

private static HttpClient _httpClient;
private readonly SynchronizationContext synchronizationContext;
public PullRequestForm(IHttpClientFactory httpClientFactory)
{
  InitializeComponent();
  synchronizationContext = SynchronizationContext.Current;
  _httpClient = httpClientFactory.CreateClient(“github”);
}

次に、PullRequestData という名前のモデルを作成して、要求を簡単に逆シリアル化できるようにします。そのためのコードは以下のようになります。

public class PullRequestData
{
  public string Url { get; set; }
  public string Title { get; set; }
}

最後に、GetPullRequestData という名前のメソッドを作成します。このメソッドでは、GitHub API に対する要求を行い、すべてのオープンな pull request を取得します。JSON 要求を逆シリアル化することになるので、最新バージョンの Newtonsoft.Json パッケージをプロジェクトに追加しておきます。コードは次のようになります。

private async Task<List<PullRequestData>> GetPullRequestData()
{
  var gitHubResponse =
    await _httpClient.GetStringAsync(
    $”repos/dotnet/winforms/pulls?state=open”);
  var gitHubData =
    JsonConvert.DeserializeObject<List<PullRequestData>>(gitHubResponse);
  return gitHubData;
}

これは、RetrieveData_Button_Click メソッドから呼び出すことができます。必要なデータの一覧ができたら、各 Title のラベルの一覧を作成して、それをフォームに表示できるようにします。ラベルの一覧ができたら、UpdateUI メソッドの UI に追加できます。図 3これを示しています。

図 3 RetrieveData_Button_Click から呼び出す内容

private async void RetrieveData_Button_Click(object sender, EventArgs e)
{
  var pullRequestData = await GetPullRequestData();
  await Task.Run(() =>
  {
    var labelsToAdd = new List<Label>();
    var verticalSpaceBetweenLabels = 20;
    var horizontalSpaceFromLeft = 10;
    for (int i = 0; i < pullRequestData.Count; i++)
    {
      Label label = new Label();
      label.Text = pullRequestData[i].Title;
      label.Left = horizontalSpaceFromLeft;
      label.Size = new Size(100, 10);
      label.AutoSize = true;
      label.Top = (i * verticalSpaceBetweenLabels);
      labelsToAdd.Add(label);
    }
    UpdateUI(labelsToAdd);
  });
}

UpdateUI メソッドでは、次のように、synchronizationContext を使用して UI を更新します。

public void UpdateUI(List<Label> labels)
{
  synchronizationContext.Post(new SendOrPostCallback(o =>
  {
    foreach (var label in labels)
    {
      Controls.Add(label);
    }
  }), labels);
}

アプリケーションを実行し、Microsoft ボタンをクリックすると、GitHub の dotnet/winforms リポジトリから取られたすべてのオープンな pull request のタイトルで UI が更新されます。

今度は、皆さんの番です。これを、この記事のタイトルにあるとおり、集中管理された pull request ハブにするために、複数の GitHub リポジトリから読み取るようにこのサンプルを更新しましょう。これらのリポジトリは、Microsoft チームからのものに限る必要はありません (もちろん、Microsoft の進展を見るのは興味深いですが)。たとえば、マイクロサービス アーキテクチャは非常に一般的で、システム全体を構成するさまざまなリポジトリが含まれています。通常、ブランチや pull request はマージしないまま長期間保持しないことが推奨されているため、このサンプルのようなツールがあれば、オープンな pull request を把握してシステム全体の品質を向上させるのに役立ちます。

確かに、Web アプリを設定することもできますが、配置、実行場所、認証などの問題に気を遣う必要があります。.NET Core の WinForms アプリケーションであれば、そのような問題をどれも気にする必要がありません。ここで、.NET Core を使用して WinForms アプリを構築することの最も大きな利点の 1 つを見てみましょう。

アプリケーションのパッケージ化

以前は、新しいまたは更新された WinForms アプリケーションを配置すると、ホスト コンピューターにインストールされている .NET Framework のバージョンに関連する問題が発生することがありました。.NET Core では、アプリを自己完結型で配置して 1 つのフォルダーから実行できるため、マシンにインストールされた .NET Framework のバージョンに依存することがありません。つまり、ユーザーは何もインストールする必要はありません。アプリケーションを実行するだけです。また、アプリを一度に 1 つずつ更新して配置することもできます。パッケージ化されたバージョンの .NET Core は相互に影響を与えないからです。

この記事のサンプル アプリは、自己完結型としてパッケージ化します。自己完結型のアプリケーションは、.NET Core ライブラリが含まれているため、サイズが大きくなることに注意してください。最新バージョンの .NET Core がインストールされているマシンに配置する場合、アプリを自己完結型にする必要はありません。代わりに、.NET Core のインストールされているバージョンを活用することで、配置されるアプリのサイズを小さくすることができます。自己完結型のオプションは、アプリケーションが実行される環境に依存しないようにするためのものです。

アプリケーションをローカルでパッケージ化するには、設定で開発者モードが有効になっていることを確認する必要があります。Visual Studio では、ユーザーがプロジェクトのパッケージ化を実行しようとすると、プロンプト画面に設定へのリンクが表示されます。Windows の設定に直接移動してそのモードを有効にするには、Windows キーを押して設定を検索することができます。検索ボックスに "開発者向け設定" と入力して、それを選択します。開発者モードを有効にするためのオプションが表示されます。このオプションを選択して有効にします。

ほとんどの場合、以前に WinForms アプリケーションをパッケージ化したことがあれば、自己完結型のパッケージを作成するステップは難しくありません。まず、新しい Windows アプリケーション パッケージ プロジェクトを作成します。新しいプロジェクトに PullRequestHubPackaging という名前を付けます。ターゲットと最小プラットフォームのバージョンを選択するようにとのメッセージが表示されたら、既定値を使用し、[OK] をクリックします。アプリケーションを右クリックし、PullRequestHub プロジェクトへの参照を追加します。

参照を追加した後は、PullRequestHub プロジェクトをエントリ ポイントとして設定する必要があります。その後、次にビルドするとき、多くの場合に次のエラーが表示されます。「Project PullRequestHub must specify ‘RuntimeIdentifiers’ in the project file when ‘SelfContained’ is true. ('SelfContained' が true の場合、プロジェクト PullRequestHub ではプロジェクト ファイルに 'RuntimeIdentifiers' を指定する必要があります。)」

このエラーを解決するには、PullRequestHub.csproj ファイルを編集します。このプロジェクト ファイルを開くと、プロジェクト ファイルが新しい軽量の形式になっていることに気づきます。これは、.NET Core を使用するもう 1 つの利点です。.NET Framework ベースの WinForms プロジェクトでは、プロジェクト ファイルに明示的な既定値と参照が含まれていて、はるかに詳細でした。さらに、NuGet 参照は packages.config ファイルに分割されていました。新しいプロジェクト ファイル形式では、パッケージ参照がプロジェクト ファイルに含まれているため、すべての依存関係を 1 か所で管理できます。

このファイルの最初の PropertyGroup ノードに、次の行を追加します。

<RuntimeIdentifiers>win-x86</RuntimeIdentifiers>

ランタイム識別子は、アプリケーションを実行するターゲット プラットフォームを識別するために使用され、.NET パッケージでは NuGet パッケージ内のプラットフォーム固有のアセットを表すために使用されます。これを追加すれば、ビルドが成功するはずです。また、Visual Studio で PullRequestHubPackaging プロジェクトをスタートアップ プロジェクトとして設定できます。

PullRequestHubPackaging.wapproj ファイルで 1 つ注目するべき点は、プロジェクトが自己完結型であることを示す設定です。ファイル内で注意を払うべきコード セクションを次に示します。

<ItemGroup>
  <ProjectReference Include=”..\PullRequestHub\PullRequestHub.csproj”>
    <DesktopBridgeSelfContained>True</DesktopBridgeSelfContained>
    <DesktopBridgeIdentifier>$(DesktopBridgeRuntimeIdentifier)
    </DesktopBridgeIdentifier>
      <Properties>SelfContained=%(DesktopBridgeSelfContained);
        RuntimeIdentifier=%(DesktopBridgeIdentifier)
      </Properties>
    <SkipGetTargetFrameworkProperties>True
    </SkipGetTargetFrameworkProperties>
  </ProjectReference>
</ItemGroup>

DesktopBridgeSelfContained オプションが true に設定されていることがわかります。これにより、WinForms アプリケーションは .NET Core バイナリと一緒にパッケージ化されます。プロジェクトを実行すると、次のようなパスの "win-x86" という名前のフォルダーにファイルがダンプされます。

C:\Your-Path\PullRequestHub\PullRequestHub\bin\x86\Debug\netcoreapp3.0

win-x86 フォルダー内には多数の DLL が入っており、自己完結型アプリの実行に必要なすべてのものが含まれています。

多くの場合、アプリはサイドロード アプリケーションとして配置するか、または Microsoft Store にアップロードします。サイドローディングすると、アプリ インストーラー ファイルを使用した自動更新が可能になります。これらの更新は、Visual Studio 2017 の Update 15.7 以降でサポートされます。配布用に Microsoft Store に提出することをサポートするパッケージを作成することもできます。アプリのコード署名、配布、更新は、すべて Microsoft Store によって処理されます。

これらのオプションに加えて、アプリケーションを 1 つの実行可能ファイルにパッケージ化できるようにするための作業が進行中です。これにより、出力ディレクトリに DLL を設定する必要がなくなります。

その他の利点

.NET Core 3.0 では C# 8.0 の機能を活用することもできます。たとえば、null 許容参照型、インターフェイスの既定の実装、パターンを使用した switch ステートメントの機能強化、非同期ストリームなどがあります。C# 8.0 を使用可能にするには、PullRequestHub.csproj ファイルを開き、最初の PropertyGroup に次の行を追加します。

<LangVersion>8.0</LangVersion>

.NET Core と WinForms を使用するもう 1 つの利点は、どちらのプロジェクトもオープンソースであることです。そのため、ソース コードにアクセスして、バグを報告したり、フィードバックを共有したり、共同作成者になったりすることができます。WinForms プロジェクトは github.com/dotnet/winforms で確認できます。

.NET Core 3.0 は、WinForms アプリケーションに対してこれまで企業が行った投資に新たな命を吹き込むものとなり、引き続き高い生産性と信頼性を維持することができ、配置と保守が容易です。開発者は、HttpClientFactory などの新しい .NET Core 固有のクラスを活用し、null 許容参照型などの C# 8.0 機能を利用し、自己完結型アプリケーションをパッケージ化することができます。また、.NET Core CLI にアクセスすることができ、.NET Core が提供するすべてのパフォーマンス向上の恩恵を受けることができます。


Eric Fleming氏は、Microsoft のツールおよびテクノロジに関して 10 年を超える作業経験を持つシニア ソフトウェア エンジニアです。ericflemingblog.com でブログを書いています。Azure Functions について探る YouTube チャンネル Function Junction を共同で主催しています。Twitter は、@efleming18 でフォローできます。

この記事のレビューに協力してくれた以下の技術スタッフに感謝します。Olia Gavrysh 氏 (Microsoft)、Simon Timms 氏


この記事について MSDN マガジン フォーラムで議論する