redwarrior’s diary

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

(小ネタ)Visual Studio Installer Projectsを使用し、アップデートに対応したインストーラーを作成する

Visual Studio Installer Projects拡張機能を使用すると、インストーラーを作成するためのプロジェクト(Setupプロジェクト)を作成できるようになる。

アプリケーションを更新するたびに、古いバージョンをアンインストールして、新しいバージョンをインストールするのは、ユーザーにとって手間なので、アップデートに対応したインストーラーを作成したい。

インターネットで調べるとやり方はヒットするのだが、なぜそのやり方になるのかという理由がなかなか見つからなかったので、調べた結果を残しておく。

アップデートの分類

デスクトップアプリケーションのアップデートは、3種類(Small Update、Minor Upgrade、Major Upgrade)に分類されている。

参考サイト:セットアッププロジェクトによるアップデート - .NET Tips (VB.NET,C#...)

アプリケーションのバージョンアップと言われたら、大体Minor Upgradeか、Small Updateを想定するだろう。

Major Upgrade以外は非対応

しかし、様々なサイトを見てみると、Major Upgradeのやり方ばかり書いてある。

これはなぜかというと、Setupプロジェクトで作成できるインストーラーは、ProductCode が同じままバージョンアップする方法(Minor Upgrade/Small Update)に対応していないからである。

参考サイト:Re[2]: msi作成について

ProductCode を変えずにインストーラーを作成すると、実行時に以下のメッセージが表示される。

f:id:redwarrior:20210216104807p:plain

Major Upgradeの設定のコツ

Major Upgradeは過去のバージョンと共存できることを想定しているため、同じフォルダを指定してインストールをしても、「プログラムと機能」に古いバージョンと新しいバージョンの両方のアプリケーションが表示されてしまう。

このため、SetupプロジェクトのRemovePreviousVersionsプロパティをTrueにして、古いバージョンをアンインストールすることで、アップデートしたように見せている。

以上

Dispatcher.InvokeとDispatcher.BeginInvoke、Dispatcher.InvokeAsyncの違い

WPFアプリケーションで、別スレッドから画面を更新したい場合は、DispatcherクラスのInvokeメソッドに、引数としてメソッドを渡して、UIスレッドで実行されるようにします。

ググってみると、DispatcherクラスにはInvokeメソッドの他に、BeginInvokeメソッド、InvokeAsyncメソッドがあるようです。

これらの違いが気になったので調べてみました。

Dispatcher.InvokeとDispatcher.BeginInvoke、Dispatcher.InvokeAsyncの違い

詳しく説明されているサイトがありました。

Learn more about how WPF Dispatcher works.(Invoke and InvokeAsync) | Neal's Blog

このサイトによると、Invokeが同期処理で、BeginInvokeとInvokeAsyncが非同期処理であり、InvokeAsyncが.NET Framework 4.5 で追加された新しいメソッドであるらしいです。

現在ではBeginInvokeとInvokeAsyncの実装は同じなので、InvokeAsyncを使うと良いそうです。

違いを確認する

文章で理解しただけだと「(* ̄- ̄)ふ~ん 」となって終わるので、コードを書いて確かめてみました。

MainWindow.xaml

<Window x:Class="DispatcherTestApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DispatcherTestApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="300">
    <StackPanel>
        <ListBox>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click" Width="140" Margin="10">Dispatcher.Invoke</Button>
                    <TextBlock Name="Text" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・①</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_1" Width="140" Margin="10">Dispatcher.BeginInvoke</Button>
                    <TextBlock Name="Text1" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・②</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_2" Width="140" Margin="10">Dispatcher.InvokeAsync</Button>
                    <TextBlock Name="Text2" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・③</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_3" Width="140" Margin="10">NoneDispatcher</Button>
                    <TextBlock Name="Text3" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・④</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_4" Width="140" Margin="10">await InvokeAsync</Button>
                    <TextBlock Name="Text4" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・⑤</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_5" Width="140" Margin="10">return value</Button>
                    <TextBlock Name="Text5" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・⑥</TextBlock>
                </StackPanel>
            </ListBoxItem>
        </ListBox>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace DispatcherTestApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly Stopwatch _stopwatch;

        public MainWindow()
        {
            _stopwatch = new Stopwatch();
            InitializeComponent();
        }

        #region Dispatcher.Invoke
        //①
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Text.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);
            //Text.Text = "456"; <--エラーになる

            Dispatcher.Invoke(ChangeText);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region Dispatcher.BeginInvoke
        //②
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            Text1.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction1);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction1()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            Dispatcher.BeginInvoke(new Action(ChangeText1));

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText1()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text1.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region Dispatcher.InvokeAsync
        //③
        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            Text2.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction2);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction2()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            Dispatcher.InvokeAsync(ChangeText2);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText2()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text2.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region NoneDispatcher
        //④
        private async void Button_Click_3(object sender, RoutedEventArgs e)
        {
            Text3.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            await Task.Run(HeavyAction3);

            ChangeText3();

            //Console.WriteLine($"{stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction3()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            //Dispatcher.InvokeAsync(ChangeText3);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText3()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text3.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region await Dispatcher.InvokeAsync
        //⑤
        private void Button_Click_4(object sender, RoutedEventArgs e)
        {
            Text4.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction4);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private async Task HeavyAction4()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            await Dispatcher.InvokeAsync(ChangeText4);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText4()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text4.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region return value
        //⑥
        private void Button_Click_5(object sender, RoutedEventArgs e)
        {
            Text5.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction5);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private async Task HeavyAction5()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            var result = await Dispatcher.InvokeAsync(ChangeText5);

            Thread.Sleep(100);

            Console.WriteLine(result
                ? $"{_stopwatch.ElapsedMilliseconds} action result=true"
                : $"{_stopwatch.ElapsedMilliseconds} action result=false");

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private bool ChangeText5()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text5.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");

            return _stopwatch.ElapsedMilliseconds % 2 == 0;
        }
        #endregion
    }
}

Dispatcher.Invoke

Dispatcher.Invokeは、同期処理です。サンプルは①です。このメソッドを基準として他のメソッドを確認していきます。
実行結果は以下の通りです。呼び出し順に処理が行われています。

0 event start
4 event end
6 action start
3007 ui change start
6007 ui change end
6108 action end

Dispatcher.BeginInvoke

Dispatcher.BeginInvokeは、非同期処理です。サンプルは②です。Delegateしか渡せないので、呼び出すときにわざわざActionをnewしなければなりません。
実行結果は以下の通りです。UI更新処理を呼び出した後も処理は続いています。

0 event start
1 event end
2 action start
3003 ui change start
3104 action end
6004 ui change end

Dispatcher.InvokeAsync

Dispatcher.InvokeAsyncは、非同期処理です。サンプルは③です。メソッドを直接渡しています。
実行結果は以下の通りです。流れはDispatcher.BeginInvokeと同じです。

0 event start
1 event end
2 action start
3003 ui change start
3103 action end
6004 ui change end

await Dispatcher.InvokeAsync

Dispatcher.BeginInvoke、Dispatcher.InvokeAsyncの返り値のDispatcherOperationクラスは、GetAwaiterメソッドを持っているので、awaitをつけることが出来ます。サンプルは⑤です。
実行結果は以下の通りです。awaitをつけて呼び出すと、渡したメソッドの実行終了後に、戻って後続の処理を実行するため、流れはDispatcher.Invokeと同じになります。

0 event start
1 event end
7 action start
3008 ui change start
6009 ui change end
6111 action end

return value

↑でawaitをつけると、Dispatcher.Invokeと同じ処理順になることがわかりましたが、awaitが必要になる場面もあります。 Dispatcher.InvokeAsyncは、Func<TResult>を渡すことができるので、awaitで渡したメソッドの返り値を取得することが出来ます。サンプルは⑥です。
実行結果は以下の通りです。画面更新メソッドの返り値によって、後続の処理の分岐をしています。

0 event start
1 event end
8 action start
3017 ui change start
6019 ui change end
6121 action result=false
6121 action end

まとめと方針

今までの内容をまとめて考えた結果、以下の方針で使ったら良いかなと思いました。

  • 画面更新が同期処理で問題なければ、Dispatcher.Invokeを使用する
  • 画面更新を非同期にしたければ、Dispatcher.InvokeAsyncを使用する
  • 画面更新を行うメソッドの返り値を使用したい場合は、Dispatcher.InvokeAsyncを使用する

補足

以下のサイトによると、例外の扱い方が違うらしいですが、画面更新で例外が発生する事はあまりないと思うので、こちらは確かめていません。

WPF Dispatcher.BeginInvokeとDispatcher.InvokeAsyncの違い - reflux flow

WPF Dispatcher BeginInvoke vs. InvokeAsync · jbe2277/waf Wiki · GitHub

(番外編)Dispatcherを使用せずに、画面を更新する

Dispatcherを使用せずに、画面を更新する方法がいくつかあるので、書いておきます。

awaitを使用して、別スレッドからの画面の更新をしない

別スレッドの処理をする場合に、Task.Run等を使うと思いますが、Task.Runの前にawaitを記述して、画面の更新処理を後ろの行に記述します。サンプルの④の処理です。

これで別スレッドの処理が終わってから、メインスレッド(UIスレッド)に戻ってくるので、画面の更新をしても例外は発生しません。

Prism等を使用して、Data Bindingによって画面を更新する

ViewModelの更新→Data BindingによるViewの更新という仕組みを使えば、別スレッドでViewModelを変更したとしても、直接画面のコントロールを触らないため、例外は発生しません。サンプルはありません。

以上です。

Prism のバージョンと Xaml.Behaviors.Wpf、System.Windows.Interactivity の対応状況を調査した

過去記事の Prismを使用したWPFアプリケーションのまとまったサンプルを作ってみた - redwarrior’s diary や、 WPFアプリのMainWindowの終了のキャンセルを、MVVMフレームワークを使用して実装する - redwarrior’s diary で使用している、prism:InvokeCommandAction ですが、含まれるパッケージが変更になったことを、以下のサイト等で知りました。

elf-mission.net

Prism の最新版を使うのであれば、パッケージを変更すれば良いのですが、諸事情により少し前のバージョンを使用する事になりました。

その時に、Prismのバージョンは下げたのですが、パッケージを戻すことを忘れてしまい、prism:InvokeCommandAction でエラーが発生して解決に時間をかけてしまったので、
後で見返せるようにPrismのバージョンと、各パッケージの関係をまとめました。

バージョン 名前空間 prism: InvokeCommandAction 必要なパッケージ
Prism 7.1 http://schemas.microsoft.com/expression/2010/interactivity OK なし(同梱)
http://schemas.microsoft.com/xaml/behaviors NG  
Prism 7.2 http://schemas.microsoft.com/expression/2010/interactivity OK なし(同梱)
http://schemas.microsoft.com/xaml/behaviors NG  
Prism 8 http://schemas.microsoft.com/expression/2010/interactivity NG  
http://schemas.microsoft.com/xaml/behaviors OK なし(同梱)

Prism 8 で Xaml.Behaviors.Wpf パッケージに置き換わったようですね。

以上

ProgressBarの表示をMVVMアーキテクチャで実装する

ProgressBarの表示って、非同期処理が入るので、意外と難しいですよね。

表示の仕方は、以下のサイトで分かりやすく説明されているのですが、素のWPFなのです。

anderson02.com

どうせならば、MVVMアーキテクチャで実装したいなと思いやってみました。

まず、Visual Studioに「Prism Template Pack拡張機能をインストールします。

次に、「Prism Blank App」プロジェクトテンプレートを使用して、プロジェクトを作成します。

そして、ProgressBarコントロールと実行ボタンをXAMLに書きます。

MainWindow.xaml

<Window x:Class="PrismProgressBarApp3.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/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="{Binding Title}" Height="350" Width="525">
    <Grid>
        <StackPanel Orientation="Horizontal" Grid.Row="0">
            <ProgressBar Height="30" Width="300" Margin="80,0,0,0"
                         Minimum="0" Maximum="100"
                         Value="{Binding ProgressValue}" />
            <Button Height="30" Width="60" Margin="30,0,0,0" Content="Run"
                    Command="{Binding RunCommand}" />
        </StackPanel>
    </Grid>
</Window>

さらに、ProgressBarとBindingする値や、Runボタンをクリックした時のコマンドをViewModelに作成します。

MainWindowViewModel.cs

using Prism.Commands;
using Prism.Mvvm;

namespace PrismProgressBarApp3.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        private string _title = "Prism Application";
        private int _progressValue;

        public string Title
        {
            get => _title;
            set => SetProperty(ref _title, value);
        }

        public int ProgressValue
        {
            get => _progressValue;
            set => SetProperty(ref _progressValue, value);
        }

        public DelegateCommand RunCommand { get; set; }

        public MainWindowViewModel()
        {
            RunCommand = new DelegateCommand(ExecuteMethod);
        }

        private void ExecuteMethod()
        {
            throw new System.NotImplementedException();
        }
    }
}

さて、これでExecuteMethodメソッドで、ProgressValueプロパティの値を増やす処理を書けば進捗状況が更新されていきます。

ただし、上記サイトの例でもそうですが、ProgressValueの値の更新は非同期に行う必要があります。さらに、非同期処理の中では、コントロールを操作できないため、UIスレッドで変更する必要がありました。

しかし、MVVMアーキテクチャで実装すると、片方の問題は気にしなくて済みました。

private void ExecuteMethod()
{
    Task.Run(() =>
    {
        for (var i = 0; i < 10; i++)
        {
            Thread.Sleep(500);
            ProgressValue += 10;
        }
    });
}

Task.Run()で別スレッドにするのですが、データバインディングによるUIへの反映がUIスレッドで行われるため、Dispatcherを使う必要がなく、簡単に作成できました。

最後に、スリープのためにTask.Delay()を使ったりすると、await/asyncの知識が必要になるため、今回の用途ではThread.Sleep()で十分だと思います。

なお、ソースコードは、GitHubで公開しています。

github.com

動作環境

以上

MSIXアプリケーションをIISで公開する時の設定

MSIXアプリケーションをIISで公開する設定は、公式サイトに説明があります。

docs.microsoft.com

必要な部分だけ抜き出すと、IISで公開するためにはMIMEの構成を追加します。MSIXアプリケーションのフォルダの親フォルダに web.config を作成し、以下を記述します。

<system.webServer>
    <!--This is to allow the web server to serve resources with the appropriate file extension-->
    <staticContent>
      <mimeMap fileExtension=".appx" mimeType="application/appx" />
      <mimeMap fileExtension=".msix" mimeType="application/msix" />
      <mimeMap fileExtension=".appxbundle" mimeType="application/appxbundle" />
      <mimeMap fileExtension=".msixbundle" mimeType="application/msixbundle" />
      <mimeMap fileExtension=".appinstaller" mimeType="application/appinstaller" />
    </staticContent>
</system.webServer>

後はMSIXアプリケーションのフォルダにある index.html を開いてインストールが出来ます。

補足

テスト用に自己署名証明書を使用している場合は、公開証明書をインストールする場合があります。

上記の設定だけだと、アプリケーションのインストール画面で、公開者証明書をクリックしたら、404になってしまったので、以下も追加する必要がありました。

<mimeMap fileExtension=".cer" mimeType="application/pkix-cert" />

以上

Visual Studio 2019 で WCF Data Services のクライアントアプリケーションを開発する

過去の記事でサーバ側のアプリケーション(Web API)について書きましたが、クライアントアプリケーションを新しく作ることになりました。

redwarrior.hateblo.jp

Visual Studio 2015で開発するならば、特に困らないのですが、Visual Studio 2019で開発したいと思い調べました。

手順

  1. Visual Studio 2019で .NET Framework のWindows フォームを選択しプロジェクトを作成しました。
  2. プロジェクトを右クリックして、追加→サービス参照と選択し、「サービス参照の追加」画面で WCF Data Services のURLを入力して、移動をクリックします。
  3. Visual Studio 2015ならば、この後サービスの欄に使用できるサービスが表示され、選択してOKを押すとサービス参照が追加されます。

Visual Studio 2019では、3番でエラーが発生してしまいます。詳細のリンクをクリックすると以下のメッセージが表示されます。

指定された OData API を追加できません。現在 OData API は、OData クライアント コード生成ツールと併用する場合にのみサポートされています。

詳細については、次の資料をご覧ください。

https://aka.ms/odatavsclientguidance

リンク先を読み進めると、OData Connected Service という拡張機能があったので、これで行けるかなと思いインストールして試してみました。

プロジェクトを右クリックして、今度は追加→接続済みサービスと選択して、「その他のサービス」から OData Connected Service を選択して、Address欄に WCF Data Services のURLを入力しました。

次へボタンをクリックして、次の画面へ向かったのですが、そこには何も表示されず・・・。この拡張機能を使うのはあきらめました。

Visual Studio 2019での開発は無理かなと思いつつ、Visual Studio Marketplace で検索して見つけた(個人が開発しているっぽい)Unchase OData Connected Service という拡張機能を使用してみたところ、こちらは動作しました!!!

以下のMS公式サイトのチュートリアルは、動作するのを確認できたので、これで開発を進めてみようと思います。

docs.microsoft.com

GitHubリポジトリ

github.com

Visual Studio Marketplace

marketplace.visualstudio.com

ちなみに、Web API側を作りたい場合も、プロジェクトテンプレートはなくなっていますが、また別の人が作った拡張機能がありました。

GitHub

github.com

Visual Studio Marketplace

marketplace.visualstudio.com

確認環境

以上

Visual Studioのファイルのプロパティのビルド アクションの「コンテンツ」と「なし」の違い

Visual Studioの ファイルのプロパティ のビルド アクションの値で「コンテンツ」と「なし」の違いを調べました。

Visual Studio上でファイルを選択して ファイルのプロパティ に表示される項目のうち、出力ディレクトリにコピー は、値を「常にコピーする」か「新しい場合はコピーする」にすると、選択したファイルが出力ディレクトリにコピーされるので、何を意味するかは明確です。

一方、ビルド アクションは、公式サイトに説明があり、これを読むと「コンテンツ」は何となくわかった気になりますが、

docs.microsoft.com

「なし」の項目には、以下のように書かれています。

ファイルはいかなる形でもビルドに含まれません。 この値は、"ReadMe" ファイルなどのドキュメント ファイルで使用できます。

「なし」はビルドに含まれないと書いてあるのに、出力ディレクトリにコピー を適切に設定していれば、出力ディレクトリにコピーされます。

ビルド アクション は「コンテンツ」した方が良いのか、「なし」のままでが良いのか疑問に思っていました。

よくわからないので、「コンテンツ」の説明にあるリンクを辿っていくと以下のサイトに情報がありました。

docs.microsoft.com

かいつまんで書くと、

「コンテンツ」は、アセンブリには含まれないけど、ビルド時にアセンブリと関連付けることができる。アセンブリとは独立しているので、個別に更新をすることができる。ビルド時にファイルが必要。XAMLからはファイル名のみで簡単に参照できる。

「なし」は、Site of Origin Filesと呼ばれるタイプを設定する方法。Site of Origin Filesは、アセンブリと関連のないファイルで、実行時に参照したい場合に使用する。こちらも個別に更新をすることができる。XAMLからはPack URI形式で参照できる。ビルド時に存在しないファイルを参照したり、URLで参照することができる。

どう使えば良いのか

XAMLで使うかどうかが判断基準になりそうです。ビルド時にプロジェクトに存在している背景やアイコン等の画像系は「コンテンツ」に設定します。

一方、XAMLで使用する事がないEXEファイルやインストーラは「なし」に設定するのが良さそうです。

検証してみた

実際に、XAMLでImageタグを2つ用意し、ビルド アクションを「コンテンツ」にした画像と、「なし」にした画像のファイル名をそれぞれ指定すると、Visual Studioのデザイナーでは両方とも表示されますが、 デバッグ実行すると、「コンテンツ」にした画像は表示され、「なし」にした画像は表示されませんでした。

確認環境

以上