msix、コード上からインストール（WPF）

msixによる配布はこちらで説明しています。
[WPFアプリのmsixによるweb配布、自動更新方法](https://qiita.com/smi/items/17cdc56c08b8a5dab58e)

今回はインストーラー経由で更新するのではなく、
コード上から直接パッケージをダウンロード、インストールする方法を説明します。


# この記事で達成したいこと
msixファイルの置いてあるサーバーには認証がかかっていて、msixの自動更新機能では通れないときに、コード上でダウンロードして、インストールしたい。

もしくは、ランチャーアプリのように、複数のmsixアプリのダウンロードやインストールを管理するアプリを作るとき


#手順
こちらで作成したプロジェクトの続きから行います。
コード上からインストールする場合も、ここまでは手順が一緒です。
[WPFアプリのmsixによるweb配布、自動更新方法](https://qiita.com/smi/items/17cdc56c08b8a5dab58e)

# クライアント側に追加していきます。

サーバーからファイルをダウンロードするクラス
(Newtonsoft.Jsonをnugetで落としてます)

```C#:RestClient.cs
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;

namespace MSIX_Test
{
    public class RestClient
    {
        protected HttpClient client { get; set; }

        public RestClient()
        {
            client = new HttpClient();
        }
        public async Task<Stream> GetStream(string api, object param = null)
        {
            var res = await client.GetAsync(GetParamApi(api, param), HttpCompletionOption.ResponseHeadersRead);
            return await res.Content.ReadAsStreamAsync();
        }

        protected string GetParamApi(string api, object param = null)
        {
            if (param != null)
            {
                var query = HttpUtility.ParseQueryString(string.Empty);
                var json = JsonConvert.SerializeObject(param);
                var jobj = JObject.Parse(json);
                foreach (var keyValue in jobj)
                {
                    query[keyValue.Key] = keyValue.Value.ToString();
                }
                api += "?" + query.ToString();
            }
            return api;
        }
    }
}

```

msixを扱うためのクラスです、msixに関する処理を書いていきます。

```C#:PackageService.cs
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Foundation;
using Windows.Management.Deployment;

namespace MSIX_Test
{
    public class PackageService
    {
        private RestClient _client { get; set; }
        private PackageManager _packageManager = new PackageManager();
        /// <summary>
        /// 現在インストールされているパッケージ
        /// </summary>
        private Package? _package { get; set; }
        /// <summary>
        /// Package.appxmanifestファイルのパッケージ化タブのパッケージ名
        /// </summary>
        private string _packageName { get; set; }
        /// <summary>
        /// インストール完了イベント
        /// </summary>
        public AsyncOperationWithProgressCompletedHandler<DeploymentResult, DeploymentProgress> InstallResultHandler { get; set; }
        /// <summary>
        /// インストールの進捗イベント
        /// </summary>
        public AsyncOperationProgressHandler<DeploymentResult, DeploymentProgress> InstallProgressHandler { get; set; }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="packageName">Package.appxmanifestファイルのパッケージ化タブのパッケージ名</param>
        public PackageService(string packageName)
        {
            _client = new RestClient();
            _packageName = packageName;
            _package = GetPackage();
        }

        /// <summary>
        /// PCにインストールされているパッケージを取得
        /// </summary>
        /// <returns></returns>
        private Package? GetPackage()
        {
            Package package = null;
            try
            {
                // パッケージの検索
                // 見つからない場合にExceptionになるのでTry
                var packages = _packageManager.FindPackagesForUser(string.Empty);
                var packageFullName = packages.FirstOrDefault(x => x.Id.Name == _packageName)?.Id.FullName ?? "";
                package = _packageManager.FindPackageForUser(string.Empty, packageFullName);
            }
            catch (Exception e)
            {
            }
            return package;
        }

        /// <summary>
        /// 指定バージョンのパッケージをサーバーからダウンロード
        /// </summary>
        /// <param name="version"></param>
        /// <returns></returns>
        private async Task DownloadPackageAsync(string version)
        {   
            var stream  = await _client.GetStream($"https://localhost:44346/MSIX", new { version });
            using (var fileStream = new FileStream(GetSaveFilePath(), FileMode.Create, FileAccess.Write, FileShare.None))
            {
                await stream.CopyToAsync(fileStream);
            }
        }

        /// <summary>
        /// msixファイルがダウンロードされるパスを取得
        /// </summary>
        /// <returns></returns>
        private string GetSaveFilePath()
        {
            var localDir = "";
            try
            {
                // UWPアプリ
                localDir = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
            }
            catch (Exception ex)
            {
                // UWPアプリでない
                localDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            }
            string saveDirectory = Path.Combine(localDir, "MSIXPackages");

            Directory.CreateDirectory(saveDirectory);

            return Path.Combine(saveDirectory, _packageName);
        }

        /// <summary>
        /// ダウンロードしたパッケージのインストール
        /// </summary>
        /// <returns></returns>
        private async Task InstallAsync()
        {
            var uri = new Uri(GetSaveFilePath());
            PackageManager packagemanager = new PackageManager();

            var asyncProgress = packagemanager.AddPackageAsync(
                uri,
                null,
                DeploymentOptions.ForceApplicationShutdown
            );

            if (InstallResultHandler != null)
            {
                asyncProgress.Completed += InstallResultHandler;
            }

            if (InstallProgressHandler != null)
            {
                asyncProgress.Progress += InstallProgressHandler;
            }

            // ただasyncProgressをawaitするとフリーズすることがあるのでTaskRun&whileで待つ
            await Task.Run(() =>
            {
                while (asyncProgress.Status == AsyncStatus.Started)
                {
                }
            });

        }

        /// <summary>
        ///  パッケージのダウンロードとインストール
        /// </summary>
        /// <param name="version"></param>
        /// <returns></returns>
        private async Task DownlogdAndInstall(string version)
        {
            await DownloadPackageAsync(version);
            await InstallAsync();
        }

        /// <summary>
        /// 現在インストールされているパッケージのバージョンと
        /// サーバーにあるバージョンを比較して、必要の場合はダウンロードとインストールを行う
        /// </summary>
        /// <returns></returns>
        public async Task UpdatePackageAsync()
        {
            var newVersion = GetNewVersion();
            if (_package == null)
            {
                // なければ新規インストール
                await DownlogdAndInstall(newVersion.ToString());
                return;
            }

            var currentVersion = new Version(GetPackageVersionText());
            if (newVersion.CompareTo(currentVersion) > 0)
            {
                await DownlogdAndInstall(newVersion.ToString());
            }
        }

        /// <summary>
        /// サーバーで指定されているパッケージのバージョン取得
        /// </summary>
        /// <returns></returns>
        private Version GetNewVersion()
        {
            // 今回はテストのため必ず、アップデートが走るように発行するパッケージより高くしておきます
            // 実際にはサーバー等から取得しましょう
            return new Version("2.0.0.0");
        }

        /// <summary>
        /// 現在インストールされているパッケージのバージョンを取得
        /// </summary>
        /// <returns></returns>
        public string GetPackageVersionText()
        {
            if (_package == null)
                return "";

            var v = _package.Id.Version;
            return string.Format("{0}.{1}.{2}.{3}", v.Major, v.Minor, v.Build, v.Revision);
        }

    }
}

```

バージョンの変化がわかりやすいように、インストールされているパッケージのバージョンを画面に表示させます。

```xml:MainWindow.xaml.cs
<Window
    x:Class="MSIX_Test.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:local="clr-namespace:MSIX_Test"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <StackPanel>
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Text="バージョン" />
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Text="{Binding Version}" />
    </StackPanel>
</Window>

```

```C#:MainWindow.xaml.cs
using System.Threading.Tasks;
using System.Windows;

namespace MSIX_Test
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new MainWindowViewModel();
           
        }
    }

    public class MainWindowViewModel
    {
        public string Version { get; set; }
        public MainWindowViewModel()
        {
            var packageService = new PackageService("994c6583-42dd-49b5-9e3a-bf85f3f8b568");
            Version = packageService.GetPackageVersionText();
            MessageBox.Show("更新します", "", MessageBoxButton.OK);
            Task.Run(async () => await packageService.UpdatePackageAsync());
        }
    }

}

```

# msixファイル配置サーバーの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/0712e2e7-518b-3c12-1d5d-4693cfd3214c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/78e3d42c-17ca-35be-ae26-a5e0118a36a2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/d26161ae-65cc-4e53-b765-fa12d0074ce4.png)


### Controllerを作成

```C#
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Threading.Tasks;

namespace MSIX_Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class MSIXController : ControllerBase
    {
        [HttpGet]
        public async Task<IActionResult> Get()
        {
            using FileStream fs = System.IO.File.Open("MSIX_Test_Package.msix", FileMode.Open);
            Response.Headers.Add("fileSize", fs.Length.ToString());
            return File(fs, "application/force-download", "");
        }
    }
}

```

後はバージョンの「1.0.0.0」と「1.0.1.0」など新旧２つのパッケージを発行して、
旧を手動インストールした状態で、サーバー直下に新しいパッケージを置いておけば、ダウンロードとインストールがされると思います。

### 発行
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/0749d952-9e6a-ee04-bb9e-8a1405144b63.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/b616178f-d004-9011-b67f-4fc08697be96.png)
インストールしておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/7494b0d9-3d43-0162-62b6-9bd0eaec5fbe.png)

同じ手順で「1.0.1.0」発行
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/7f9c02c9-c0b9-b9a4-3d3b-1dcfe2bf758d.png)
Server直下に名前(Controllerで指定したやつに)を変えて置く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/cf03f2f5-a365-c5c0-ee4a-4aecff446a01.png)


VisualStudioでサーバープロジェクトを起動してから、アプリを起動します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/92c88364-9483-75ef-307c-1c6d207af5d2.png)

更新ダイアログの後にバージョン「1.0.0.0」が表示されたあと、アプリが終了すると思います(インストールさされたから)
もう一度アプリを起動すると、更新ダイアログの後のバージョンが「1.0.1.0」に変わると思います。

以上でアプリの更新は終了です。
結構端折ってる箇所や、抜けてる部分があるかもしれないので、気づいたらご連絡いただければ幸いです。


コードはgithubにあげてあります。
https://github.com/shinnosukekubo/MSIX_Test


他にもこんなことができるので、機会があれば記事にしようかと思います。

・インストール後に自動で再起動（PCの設定によっては機能しない端末もありました。起動後60秒立たないと機能しない制約があります）
キーワード：RegisterApplicationRestart、RestartFlags

・PCにインストールされた別のmsixアプリを起動する
キーワード：LauncherOptions、TargetApplicationPackageFamilyName、Windows.System.Launcher.LaunchUriAsync