msixによる配布はこちらで説明しています。
WPFアプリのmsixによるweb配布、自動更新方法
今回はインストーラー経由で更新するのではなく、
コード上から直接パッケージをダウンロード、インストールする方法を説明します。
この記事で達成したいこと
msixファイルの置いてあるサーバーには認証がかかっていて、msixの自動更新機能では通れないときに、コード上でダウンロードして、インストールしたい。
もしくは、ランチャーアプリのように、複数のmsixアプリのダウンロードやインストールを管理するアプリを作るとき
手順
こちらで作成したプロジェクトの続きから行います。
コード上からインストールする場合も、ここまでは手順が一緒です。
WPFアプリのmsixによるweb配布、自動更新方法
クライアント側に追加していきます。
サーバーからファイルをダウンロードするクラス
(Newtonsoft.Jsonをnugetで落としてます)
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に関する処理を書いていきます。
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);
}
}
}
バージョンの変化がわかりやすいように、インストールされているパッケージのバージョンを画面に表示させます。
<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>
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ファイル配置サーバーの作成
Controllerを作成
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」など新旧2つのパッケージを発行して、
旧を手動インストールした状態で、サーバー直下に新しいパッケージを置いておけば、ダウンロードとインストールがされると思います。
発行
同じ手順で「1.0.1.0」発行

Server直下に名前(Controllerで指定したやつに)を変えて置く

VisualStudioでサーバープロジェクトを起動してから、アプリを起動します

更新ダイアログの後にバージョン「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





