◆ 新しいバージョンを自動でチェックして 新バージョンがあれば通知
◆ アップデートするを選択すると 自動で更新
◆ 次に起動したら新しくなってる

これも昔作った物の紹介です

ウェブページだとサーバを更新すれば 次にページを開いた時に自動で更新されています
C# のデスクトップアプリだと exe ファイルを置き換えないと更新されません

アクセスしてる API や DB が変わったからの更新なら更新しないと使いものにならないので必須ですが ローカルで完結していて必須な更新でないなら 見た目の好みなどアップデートしたくないという場合もあります
古いバージョンがいいならそのままにもして置けるのが exe ファイルの場合のメリットでもあるので 強制アップデートにするのはどうかなとも思います

そういうわけで 単純に更新の有無をチェックしてあったら更新しますか?と聞いてくれるものにしました

仕組み

こういうフォルダ構成にしておきます

-- app\
---- current (link)
---- src\
---- versions\
------ 1.2.0\
-------- app.exe
-------- resources\
------ 1.3.0\
-------- app.exe
-------- resources\

アプリケーションのフォルダの中の versions フォルダにバージョン名のフォルダを作って そのバージョンのデータを入れておきます

起動すると一定の間隔で指定されたウェブページにアクセスして最新のバージョン情報を取得します
そのバージョンがローカルに存在せず今のバージョンより新しいなら アップデートの確認ダイアログを表示します

「はい」 が選択されると src フォルダに zip をダウンロードして versions フォルダに解凍します
今のところ src にダウンロードした zip は残さず消してるのでこのフォルダ要らなかったかもしれません

また current という名前で解凍されたフォルダへのジャンクション (シンボリックリンクみたいなもの) を作ります
バージョンごとに別フォルダなのですが 使う側は app\current\app.exe を実行するだけでアップデートされたバージョンを使えるようになります

過去のバージョンをわざわざ残す必要もないと思ったのですが 置き換えようにも自分自身を置き換えることになります
プログラムが動いてる間はそのファイルの削除も移動もできません
別のファイルに保存して 新しいバージョン起動時に古いのを消すことは出来ます
ですが 一時的に 2 つあるのはなんか嫌ですし 起動するべきファイルが変わるのでシンボリックリンクなどで常に同じものを開いて動くようにを作る必要があります
それならいっその事 全部のバージョンを残すようにしようということでこうなりました

軽いものばかりと考えてこうなってますが 重いものだと全バージョン残すと容量がすごいことになるので 使うなら最大いくつとか制限つけたほうがいいのかもしれませんね


ウェブぺージ側は

-- app/
---- 1.2.0.zip
---- 1.3.0.zip
---- latest

という構造になってます
latest というテキストファイルに最新のバージョン名を入れます

1.2.0

とか

ソース


[MainWindow.xaml]
<Window x:Class="UpdateExample.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:UpdateExample"
mc:Ignorable="d"
Title="UpdateExample" Height="360" Width="480" FontFamily="Meiryo">
<Viewbox>
<TextBlock Text="{Binding Source={x:Static local:App.version}}"/>
</Viewbox>
</Window>

アップデートのテスト用なのでメイン画面は何もせずただバージョンを表示するだけです

App.xaml ではビルドアクションを Page にして XAML の Application タグから StartupUri 属性を削除します

[App.xaml.cs]
using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Windows;

namespace UpdateExample
{
public partial class App : Application
{
/// <summary>
/// Application Entry Point.
/// </summary>
[System.STAThreadAttribute()]
static public void Main()
{
var app = new App();
app.InitializeComponent();
app.Startup += App_Startup;
app.Run();
}

private static void App_Startup(object sender, StartupEventArgs e)
{
new MainWindow().Show();

var dtimer = new System.Windows.Threading.DispatcherTimer();
dtimer.Tick += new EventHandler(dtimer_Tick);
dtimer.Interval = App.check_span;
dtimer.Start();
}

private static async void dtimer_Tick(object sender, EventArgs e)
{
try
{
var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(5);

var base_url = App.base_url;
var res = await client.GetAsync(base_url + "latest");
var latest_version = await res.Content.ReadAsStringAsync();

var latest_folder = App.versions_folder + @"\" + latest_version;
var latest_zip = App.tmp_folder + @"\" + latest_version + ".zip";
var exists = Directory.Exists(latest_folder);

if (!exists && isNew(version, latest_version))
{
var answer = MessageBox.Show("最新版があります。更新しますか?", "確認", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer == MessageBoxResult.Yes)
{
File.Delete(latest_zip);

using (var wclient = new WebClient())
{
wclient.DownloadFile(base_url + latest_version + ".zip", latest_zip);
}

ZipFile.ExtractToDirectory(latest_zip, App.versions_folder);
File.Delete(latest_zip);

var current = App.root_folder + @"\current";
// delete old junction
if (Directory.Exists(current))
{
Directory.Delete(current);
}

makeJunction(current, latest_folder);

MessageBox.Show("インストールしました。再起動してください。", "成功");
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}

private static bool isNew(string current, string target)
{
var ca = current.Split('.');
var ta = target.Split('.');
var len = Math.Min(ca.Length, ta.Length);

for (var i = 0; i < len; i++)
{
int ci, ti;
if (!int.TryParse(ca[i], out ci) | !int.TryParse(ta[i], out ti))
{
return false;
}

if (ci < ti)
{
return true;
}
if (ci > ti)
{
return false;
}
}

return ca.Length < ta.Length;
}

private static void makeJunction(string link_name, string target_name)
{
var process = new System.Diagnostics.Process()
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden,
FileName = "cmd.exe",
Arguments = $@"/C mklink /J ""{link_name}"" ""{target_name}""",
}
};
process.Start();
}

public static string version { get; } = "1.4.0";
public static TimeSpan check_span { get; } = new TimeSpan(0, 0, 15);
public static string base_url { get; } = "http://localhost/app/";
public static string app_folder { get; } = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\');
public static string versions_folder { get; }
public static string tmp_folder { get; }
public static string root_folder { get; }

static App()
{
if (App.app_folder.EndsWith("current"))
{
App.root_folder = Directory.GetParent(app_folder).FullName;
App.versions_folder = root_folder + @"\versions";
}
else
{
App.versions_folder = Directory.GetParent(app_folder).FullName;
App.root_folder = Directory.GetParent(versions_folder).FullName;
}

App.tmp_folder = root_folder + @"\src";

if (!Directory.Exists(App.versions_folder))
{
Directory.CreateDirectory(App.versions_folder);
}

if (!Directory.Exists(App.tmp_folder))
{
Directory.CreateDirectory(App.tmp_folder);
}
}
}
}

zip 解凍するために 参照に System.IO.Compression.FileSystem を追加する必要があります
version や check_span などを変えて設定を変えられます

実行例

起動するとこんな画面です

csupdate01-01

今回は 15 秒間隔に設定しているので 15 秒後に更新があると

csupdate01-02

のダイアログが出ます
「はい」 を選べば最新バージョンのフォルダに保存されて 再起動してください とメッセージが出ます

再起動すると

csupdate01-03

とバージョンが上がりました

C# で自動アップデートする 2 へ続きます