Розгортання WPF застосунків
Розгортання WPF застосунків
Створення WPF додатку — це лише перша частина шляху до користувача. Щоб ваш додаток потрапив на комп'ютери користувачів та працював надійно, його потрібно правильно зібрати, запакувати та розповсюдити. Windows має багату історію технологій розгортання — від простого копіювання файлів до складних MSI інсталяторів, від ClickOnce до сучасного MSIX. Кожна технологія має свої переваги, недоліки та сценарії використання.
У цій статті ми детально розглянемо всі аспекти розгортання WPF додатків: від базової збірки через dotnet publish до створення професійних інсталяторів, від вибору між self-contained та framework-dependent deployment до налаштування автоматичних оновлень, від code signing для довіри користувачів до CI/CD pipeline для автоматизації процесу. Ми також обговоримо специфічні для Windows аспекти: UAC elevation, реєстр, shortcuts, file associations та інші деталі, які роблять додаток справді "нативним" для Windows.
Правильне розгортання — це не просто технічна задача, а важлива частина user experience. Користувач повинен мати можливість легко встановити ваш додаток, отримувати автоматичні оновлення, бути впевненим у безпеці (через code signing), та мати можливість чисто видалити додаток коли він більше не потрібен. Давайте розберемося, як це зробити правильно.
- Deployment — розгортання, процес підготовки та встановлення додатку
- Self-contained — автономний додаток, який включає .NET Runtime
- Framework-dependent — додаток, який вимагає встановленого .NET Runtime
- ClickOnce — технологія Microsoft для web-based deployment з auto-updates
- MSIX — сучасний формат пакування для Windows (замінник AppX)
- MSI — Windows Installer, традиційний формат інсталяторів
- Code Signing — цифровий підпис додатку для підтвердження автентичності
- UAC — User Account Control, система контролю доступу Windows
- Squirrel — open-source фреймворк для auto-updates
Базова збірка WPF додатку
Перш ніж створювати інсталятори, потрібно правильно зібрати додаток. WPF додатки збираються через dotnet publish або MSBuild.
dotnet publish — базова команда
dotnet publish -c Release
Ця команда:
- Компілює проєкт у Release конфігурації (з оптимізаціями)
- Збирає всі NuGet залежності
- Копіює XAML, images, resources
- Створює output у папці
bin/Release/net8.0-windows/publish/
Self-contained vs Framework-dependent
Framework-dependent (за замовчуванням):
dotnet publish -c Release --self-contained false
Переваги:
- Малий розмір (~5-15 MB)
- Швидше завантажується
- Автоматичні security patches через Windows Update
Недоліки:
- Користувач повинен встановити .NET Desktop Runtime
- Залежність від версії runtime
Self-contained (включає runtime):
dotnet publish -c Release --self-contained true -r win-x64
Переваги:
- Працює "out of the box" без встановлення runtime
- Контроль над версією runtime
- Простіше для користувачів
Недоліки:
- Великий розмір (~70-120 MB)
- Потрібно вручну оновлювати для security patches
- Windows користувачі звикли встановлювати prerequisites
- Багато додатків використовують .NET, тому runtime вже встановлений
- Менший розмір інсталятора
- Автоматичні security updates
- Додаток розповсюджується через portable ZIP
- Потрібна специфічна версія runtime
- Цільова аудиторія — non-technical користувачі
Оптимізація розміру через Trimming
Для self-contained deployment можна зменшити розмір через trimming:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
</PropertyGroup>
dotnet publish -c Release --self-contained true -r win-x64 -p:PublishTrimmed=true
Результат: Зменшення з ~100 MB до ~50-60 MB.
Обережно: WPF використовує багато reflection, тому trimming може зламати додаток. Завжди тестуйте після trimming!
Single-file deployment
Упакування всього додатку в один .exe файл:
dotnet publish -c Release --self-contained true -r win-x64 -p:PublishSingleFile=true
Переваги:
- Один файл замість папки з десятками DLL
- Зручно для portable версій
Недоліки:
- При запуску розпаковується у temp папку (повільніший старт)
- Може не працювати з деякими бібліотеками
- WPF resources (XAML, images) можуть мати проблеми
Рекомендація: Для WPF краще використовувати звичайний publish без single-file, а потім створити інсталятор.
ClickOnce — найпростіший спосіб deployment
ClickOnce — це технологія Microsoft для web-based deployment з автоматичними оновленнями. Користувач відкриває URL, клікає "Install", і додаток встановлюється та автоматично оновлюється.
Переваги ClickOnce
Недоліки ClickOnce
- Обмежені можливості кастомізації установки
- Не підходить для enterprise deployment (GPO, SCCM)
- Складно інтегрувати з системою (file associations, registry)
- Застаріла технологія (Microsoft рекомендує MSIX)
Налаштування ClickOnce у Visual Studio
Крок 1: Правий клік на проєкті → Properties → Publish
Крок 2: Налаштування параметрів
<!-- У .csproj -->
<PropertyGroup>
<PublishUrl>https://myapp.com/deploy/</PublishUrl>
<InstallUrl>https://myapp.com/deploy/</InstallUrl>
<UpdateEnabled>true</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<ApplicationRevision>1</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>true</IsWebBootstrapper>
<ProductName>My WPF App</ProductName>
<PublisherName>My Company</PublisherName>
</PropertyGroup>
Крок 3: Publish
# Через Visual Studio: Build → Publish
# Або через CLI:
msbuild /t:Publish /p:Configuration=Release /p:PublishDir=C:\Deploy\
Крок 4: Завантажити файли на web-сервер
ClickOnce генерує:
setup.exe— bootstrapper для установкиMyApp.application— manifest файл- Папка
Application Filesз версіями додатку
Крок 5: Користувач відкриває https://myapp.com/deploy/setup.exe та встановлює
Auto-updates у ClickOnce
ClickOnce автоматично перевіряє оновлення:
Foreground updates (перевірка при старті):
<UpdateMode>Foreground</UpdateMode>
Додаток перевіряє оновлення при старті. Якщо є нова версія — завантажує та перезапускається.
Background updates (перевірка у фоні):
<UpdateMode>Background</UpdateMode>
Додаток перевіряє оновлення у фоні. Нова версія встановлюється при наступному запуску.
Програмна перевірка оновлень:
using System.Deployment.Application;
public class UpdateService
{
public async Task<bool> CheckForUpdatesAsync()
{
if (!ApplicationDeployment.IsNetworkDeployed)
{
// Не ClickOnce deployment
return false;
}
var deployment = ApplicationDeployment.CurrentDeployment;
try
{
var updateInfo = await Task.Run(() =>
deployment.CheckForDetailedUpdate());
if (updateInfo.UpdateAvailable)
{
var result = MessageBox.Show(
$"New version {updateInfo.AvailableVersion} is available. Update now?",
"Update Available",
MessageBoxButton.YesNo);
if (result == MessageBoxResult.Yes)
{
await Task.Run(() => deployment.Update());
MessageBox.Show("Update installed. Please restart the application.");
return true;
}
}
}
catch (DeploymentDownloadException ex)
{
MessageBox.Show($"Update failed: {ex.Message}");
}
return false;
}
}
- Потрібна підтримка старих версій Windows (XP, Vista)
- Вже є існуюча ClickOnce інфраструктура
- Потрібен максимально простий deployment без інсталятора
MSIX — сучасний формат пакування
MSIX — це сучасний формат пакування для Windows, який замінює AppX та є рекомендованим способом розповсюдження додатків через Microsoft Store та enterprise deployment.
Переваги MSIX
Недоліки MSIX
- Потрібен Windows 10 1809+ (не працює на Windows 7/8)
- Обов'язковий code signing (потрібен сертифікат)
- Обмеження sandbox (не всі API доступні)
- Складніше налаштувати ніж MSI
Створення MSIX пакету
Спосіб 1: Visual Studio (найпростіший)
- Додайте Windows Application Packaging Project до solution
- Правий клік → Add → Existing Project → виберіть WPF проєкт
- Налаштуйте Package.appxmanifest
- Build → Create App Packages
Спосіб 2: MSIX Packaging Tool
Microsoft надає MSIX Packaging Tool для конвертації існуючих інсталяторів у MSIX.
Спосіб 3: CLI через makeappx
# Створити MSIX з папки
makeappx pack /d C:\MyApp\publish /p C:\MyApp\MyApp.msix
# Підписати MSIX
signtool sign /fd SHA256 /a /f MyCertificate.pfx /p password C:\MyApp\MyApp.msix
Package.appxmanifest — конфігурація
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities">
<Identity Name="MyCompany.MyWPFApp"
Publisher="CN=My Company"
Version="1.0.0.0" />
<Properties>
<DisplayName>My WPF App</DisplayName>
<PublisherDisplayName>My Company</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop"
MinVersion="10.0.17763.0"
MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
<Resource Language="uk-ua" />
</Resources>
<Applications>
<Application Id="MyWPFApp"
Executable="MyWPFApp.exe"
EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements DisplayName="My WPF App"
Description="My awesome WPF application"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<!-- File associations -->
<Extensions>
<uap:Extension Category="windows.fileTypeAssociation">
<uap:FileTypeAssociation Name="myapp">
<uap:SupportedFileTypes>
<uap:FileType>.myapp</uap:FileType>
</uap:SupportedFileTypes>
</uap:FileTypeAssociation>
</uap:Extension>
</Extensions>
</Application>
</Applications>
<!-- Capabilities -->
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<Capability Name="internetClient" />
</Capabilities>
</Package>
MSIX Auto-updates
MSIX підтримує автоматичні оновлення через:
1. Microsoft Store (найпростіше):
- Завантажте MSIX у Microsoft Store
- Оновлення автоматичні через Store
2. Власний сервер (для enterprise):
Створіть .appinstaller файл:
<?xml version="1.0" encoding="utf-8"?>
<AppInstaller Uri="https://myapp.com/MyApp.appinstaller"
Version="1.0.0.0"
xmlns="http://schemas.microsoft.com/appx/appinstaller/2017/2">
<MainPackage Name="MyCompany.MyWPFApp"
Publisher="CN=My Company"
Version="1.0.0.0"
Uri="https://myapp.com/MyApp_1.0.0.0.msix"
ProcessorArchitecture="x64" />
<UpdateSettings>
<OnLaunch HoursBetweenUpdateChecks="12" />
<AutomaticBackgroundTask />
</UpdateSettings>
</AppInstaller>
Користувач встановлює через:
https://myapp.com/MyApp.appinstaller
Windows автоматично перевіряє оновлення кожні 12 годин.
# Створити self-signed сертифікат
New-SelfSignedCertificate -Type CodeSigningCert `
-Subject "CN=My Company" `
-KeyUsage DigitalSignature `
-FriendlyName "My Company Code Signing" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3")
# Експортувати у PFX
$cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object {$_.Subject -eq "CN=My Company"}
Export-PfxCertificate -Cert $cert -FilePath MyCert.pfx -Password (ConvertTo-SecureString -String "password" -Force -AsPlainText)
- DigiCert, Sectigo, GlobalSign — від $200/рік
- Для Microsoft Store — сертифікат не потрібен (Store підписує автоматично)
MSI — традиційні інсталятори
MSI (Microsoft Installer) — це традиційний формат інсталяторів для Windows, який існує з Windows 2000. Хоча MSIX є сучаснішим, MSI все ще широко використовується, особливо в enterprise середовищі.
Переваги MSI
/quiet параметр для автоматизації.Недоліки MSI
- Складніше створювати ніж ClickOnce або MSIX
- Немає вбудованих auto-updates (потрібні сторонні рішення)
- Може залишати сміття при видаленні
- Потрібен UAC elevation (administrator права)
Створення MSI через WiX Toolset
WiX (Windows Installer XML) — найпопулярніший open-source інструмент для створення MSI інсталяторів.
Установка WiX:
dotnet tool install --global wix
Створення WiX проєкту:
<!-- Product.wxs -->
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*"
Name="My WPF App"
Language="1033"
Version="1.0.0.0"
Manufacturer="My Company"
UpgradeCode="12345678-1234-1234-1234-123456789012">
<Package InstallerVersion="200"
Compressed="yes"
InstallScope="perMachine" />
<!-- Upgrade logic -->
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
<!-- Media -->
<MediaTemplate EmbedCab="yes" />
<!-- Features -->
<Feature Id="ProductFeature" Title="My WPF App" Level="1">
<ComponentGroupRef Id="ProductComponents" />
<ComponentRef Id="ApplicationShortcut" />
</Feature>
</Product>
<!-- Directory structure -->
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="MyWPFApp" />
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="My WPF App"/>
</Directory>
<Directory Id="DesktopFolder" Name="Desktop" />
</Directory>
</Fragment>
<!-- Components -->
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<!-- Main executable -->
<Component Id="MainExecutable" Guid="*">
<File Id="MyWPFAppExe"
Source="$(var.MyWPFApp.TargetPath)"
KeyPath="yes" />
</Component>
<!-- Dependencies -->
<Component Id="Dependencies" Guid="*">
<File Id="Dependency1" Source="$(var.MyWPFApp.TargetDir)SomeDependency.dll" />
<!-- Додайте всі DLL -->
</Component>
<!-- Registry entries -->
<Component Id="RegistryEntries" Guid="*">
<RegistryKey Root="HKCU" Key="Software\MyCompany\MyWPFApp">
<RegistryValue Type="string" Name="InstallPath" Value="[INSTALLFOLDER]" KeyPath="yes" />
</RegistryKey>
</Component>
</ComponentGroup>
<!-- Shortcuts -->
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="My WPF App"
Description="My awesome WPF application"
Target="[INSTALLFOLDER]MyWPFApp.exe"
WorkingDirectory="INSTALLFOLDER"/>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU"
Key="Software\MyCompany\MyWPFApp"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes"/>
</Component>
</DirectoryRef>
<!-- Desktop shortcut -->
<DirectoryRef Id="DesktopFolder">
<Component Id="DesktopShortcut" Guid="*">
<Shortcut Id="DesktopShortcut"
Name="My WPF App"
Target="[INSTALLFOLDER]MyWPFApp.exe"
WorkingDirectory="INSTALLFOLDER"/>
<RegistryValue Root="HKCU"
Key="Software\MyCompany\MyWPFApp"
Name="DesktopShortcut"
Type="integer"
Value="1"
KeyPath="yes"/>
</Component>
</DirectoryRef>
</Fragment>
</Wix>
Збірка MSI:
# Compile .wxs to .wixobj
wix build Product.wxs -o MyWPFApp.msi
# З прив'язкою до WPF проєкту
wix build Product.wxs -ext WixToolset.UI.wixext -o MyWPFApp.msi
Перевірка .NET Runtime
MSI може перевірити наявність .NET Runtime та встановити його автоматично:
<!-- У Product.wxs -->
<PropertyRef Id="NETFRAMEWORK48"/>
<Condition Message="This application requires .NET Framework 4.8 or higher.">
<![CDATA[Installed OR NETFRAMEWORK48]]>
</Condition>
<!-- Або для .NET 8 -->
<Property Id="DOTNET8INSTALLED">
<RegistrySearch Id="DotNet8Registry"
Root="HKLM"
Key="SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedhost"
Name="Version"
Type="raw" />
</Property>
<Condition Message="This application requires .NET 8 Desktop Runtime.">
<![CDATA[Installed OR DOTNET8INSTALLED]]>
</Condition>
Bootstrapper для установки prerequisites
Для автоматичної установки .NET Runtime створіть bootstrapper через WiX Burn:
<!-- Bundle.wxs -->
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:bal="http://schemas.microsoft.com/wix/BalExtension"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Bundle Name="My WPF App Installer"
Version="1.0.0.0"
Manufacturer="My Company"
UpgradeCode="12345678-1234-1234-1234-123456789012">
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
<bal:WixStandardBootstrapperApplication
LicenseFile="License.rtf"
LogoFile="Logo.png"
ThemeFile="Theme.xml" />
</BootstrapperApplicationRef>
<Chain>
<!-- .NET 8 Desktop Runtime -->
<ExePackage Id="DotNet8Runtime"
DisplayName=".NET 8 Desktop Runtime"
SourceFile="windowsdesktop-runtime-8.0.0-win-x64.exe"
InstallCommand="/install /quiet /norestart"
DetectCondition="DOTNET8INSTALLED"
Permanent="yes" />
<!-- Main MSI -->
<MsiPackage Id="MainMsi"
SourceFile="MyWPFApp.msi"
DisplayInternalUI="yes" />
</Chain>
</Bundle>
</Wix>
Це створить setup.exe, який:
- Перевірить наявність .NET 8 Runtime
- Якщо немає — встановить автоматично
- Встановить ваш додаток
Silent installation
MSI підтримує тиху установку для автоматизації:
# Тиха установка
msiexec /i MyWPFApp.msi /quiet /norestart
# Тиха установка з логом
msiexec /i MyWPFApp.msi /quiet /norestart /l*v install.log
# Тиха установка у custom папку
msiexec /i MyWPFApp.msi /quiet INSTALLFOLDER="C:\MyCustomPath"
# Тихе видалення
msiexec /x MyWPFApp.msi /quiet /norestart
Це корисно для:
- Enterprise deployment через GPO або SCCM
- Автоматизованого тестування
- CI/CD pipelines
- GUI для створення MSI
- Wizard-based підхід
- Вбудована підтримка .NET detection
- Visual Studio integration
- Простіший ніж WiX
- Script-based конфігурація
- Створює .exe інсталятор (не MSI)
- Популярний у open-source спільноті
- Дуже гнучкий
- Script-based
- Малий розмір інсталятора
- Складніший синтаксис
Squirrel — auto-updates для desktop додатків
Squirrel — це open-source фреймворк для автоматичних оновлень desktop додатків. На відміну від ClickOnce та MSIX, Squirrel дає повний контроль над процесом оновлення.
Переваги Squirrel
Установка Squirrel
dotnet add package Squirrel.Windows
Інтеграція Squirrel у WPF додаток
App.xaml.cs:
using Squirrel;
using System.Windows;
public partial class App : Application
{
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Squirrel hooks для інсталяції/деінсталяції
SquirrelAwareApp.HandleEvents(
onInitialInstall: OnAppInstall,
onAppUpdate: OnAppUpdate,
onAppUninstall: OnAppUninstall,
onFirstRun: OnFirstRun);
// Перевірка оновлень
await CheckForUpdatesAsync();
}
private static void OnAppInstall(SemanticVersion version, IAppTools tools)
{
// Створити shortcuts
tools.CreateShortcutForThisExe(ShortcutLocation.StartMenu | ShortcutLocation.Desktop);
}
private static void OnAppUpdate(SemanticVersion version, IAppTools tools)
{
// Оновити shortcuts
tools.CreateShortcutForThisExe(ShortcutLocation.StartMenu | ShortcutLocation.Desktop);
}
private static void OnAppUninstall(SemanticVersion version, IAppTools tools)
{
// Видалити shortcuts
tools.RemoveShortcutForThisExe(ShortcutLocation.StartMenu | ShortcutLocation.Desktop);
}
private static void OnFirstRun()
{
// Перший запуск після установки
MessageBox.Show("Thank you for installing My WPF App!");
}
private async Task CheckForUpdatesAsync()
{
try
{
using var updateManager = new UpdateManager("https://myapp.com/releases");
// Перевірити оновлення
var updateInfo = await updateManager.CheckForUpdate();
if (updateInfo.ReleasesToApply.Any())
{
// Завантажити та застосувати оновлення
await updateManager.UpdateApp();
// Показати notification
MessageBox.Show(
"Update downloaded. It will be applied on next restart.",
"Update Available",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
}
catch (Exception ex)
{
// Помилка оновлення не повинна ламати додаток
System.Diagnostics.Debug.WriteLine($"Update check failed: {ex.Message}");
}
}
}
Створення Squirrel release
Крок 1: Створити NuGet пакет з вашим додатком
<!-- У .csproj -->
<PropertyGroup>
<PackageId>MyWPFApp</PackageId>
<Version>1.0.0</Version>
<Authors>My Company</Authors>
<Description>My awesome WPF application</Description>
</PropertyGroup>
dotnet pack -c Release
Крок 2: Створити Squirrel release
# Встановити Squirrel CLI
dotnet tool install --global Squirrel.Tool
# Створити release
squirrel pack --packId MyWPFApp --packVersion 1.0.0 --packDirectory ./bin/Release/net8.0-windows/publish
Це створить:
MyWPFApp-1.0.0-full.nupkg— повний пакетSetup.exe— інсталяторRELEASES— файл з інформацією про версії
Крок 3: Завантажити на сервер
https://myapp.com/releases/
├── RELEASES
├── MyWPFApp-1.0.0-full.nupkg
└── Setup.exe
Крок 4: Для наступної версії створити delta update
squirrel pack --packId MyWPFApp --packVersion 1.1.0 --packDirectory ./bin/Release/net8.0-windows/publish --delta ./releases
Це створить:
MyWPFApp-1.1.0-full.nupkg— повний пакетMyWPFApp-1.1.0-delta.nupkg— тільки зміни (малий розмір!)- Оновлений
RELEASESфайл
Програмний контроль оновлень
Squirrel надає повний контроль над процесом оновлення:
public class UpdateService
{
private readonly UpdateManager _updateManager;
public UpdateService(string updateUrl)
{
_updateManager = new UpdateManager(updateUrl);
}
// Перевірка оновлень з прогресом
public async Task<UpdateInfo?> CheckForUpdatesAsync(IProgress<int> progress)
{
try
{
var updateInfo = await _updateManager.CheckForUpdate(progress: progress);
return updateInfo.ReleasesToApply.Any() ? updateInfo : null;
}
catch (Exception ex)
{
Debug.WriteLine($"Update check failed: {ex.Message}");
return null;
}
}
// Завантаження оновлення з прогресом
public async Task<string?> DownloadUpdatesAsync(IProgress<int> progress)
{
try
{
var release = await _updateManager.UpdateApp(progress: progress);
return release?.Version.ToString();
}
catch (Exception ex)
{
Debug.WriteLine($"Update download failed: {ex.Message}");
return null;
}
}
// Застосування оновлення та перезапуск
public void ApplyUpdatesAndRestart()
{
UpdateManager.RestartApp();
}
// Rollback до попередньої версії (якщо щось пішло не так)
public async Task RollbackAsync()
{
// Squirrel зберігає попередні версії
// Можна повернутися до них програмно
}
public void Dispose()
{
_updateManager?.Dispose();
}
}
Використання у ViewModel:
public class MainViewModel : ViewModelBase
{
private readonly UpdateService _updateService;
private string _updateStatus = "Checking for updates...";
private int _updateProgress;
public MainViewModel(UpdateService updateService)
{
_updateService = updateService;
CheckForUpdatesCommand = ReactiveCommand.CreateFromTask(CheckForUpdatesAsync);
}
public string UpdateStatus
{
get => _updateStatus;
set => this.RaiseAndSetIfChanged(ref _updateStatus, value);
}
public int UpdateProgress
{
get => _updateProgress;
set => this.RaiseAndSetIfChanged(ref _updateProgress, value);
}
public ICommand CheckForUpdatesCommand { get; }
private async Task CheckForUpdatesAsync()
{
var progress = new Progress<int>(p => UpdateProgress = p);
UpdateStatus = "Checking for updates...";
var updateInfo = await _updateService.CheckForUpdatesAsync(progress);
if (updateInfo != null)
{
UpdateStatus = $"Update available: {updateInfo.FutureReleaseEntry.Version}";
var result = MessageBox.Show(
"New version available. Download now?",
"Update Available",
MessageBoxButton.YesNo);
if (result == MessageBoxResult.Yes)
{
UpdateStatus = "Downloading update...";
var newVersion = await _updateService.DownloadUpdatesAsync(progress);
if (newVersion != null)
{
UpdateStatus = $"Update {newVersion} ready. Restart to apply.";
var restartResult = MessageBox.Show(
"Update downloaded. Restart now?",
"Restart Required",
MessageBoxButton.YesNo);
if (restartResult == MessageBoxResult.Yes)
{
_updateService.ApplyUpdatesAndRestart();
}
}
}
}
else
{
UpdateStatus = "You're up to date!";
}
}
}
dotnet add package Clowd.Squirrel
- Активно підтримується
- Підтримка .NET 6/7/8
- Покращена продуктивність
- Більше features (portable mode, custom installers)
Code Signing — цифровий підпис
Code signing — це процес цифрового підпису вашого додатку для підтвердження автентичності та цілісності. Це критично важливо для довіри користувачів та уникнення попереджень Windows SmartScreen.
Чому потрібен code signing?
Без code signing:
- Windows SmartScreen показує попередження "Unknown publisher"
- Користувачі бачать страшне повідомлення при запуску
- Антивіруси більш підозріло ставляться до додатку
- Неможливо опублікувати у Microsoft Store
З code signing:
- Додаток показує ваше ім'я як publisher
- Немає попереджень SmartScreen (після набуття репутації)
- Користувачі довіряють додатку
- Можливість публікації у Store
Отримання code signing сертифікату
Для розробки (self-signed, безкоштовно):
# Створити self-signed сертифікат
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject "CN=My Company, O=My Company, C=US" `
-KeyUsage DigitalSignature `
-FriendlyName "My Company Code Signing" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3") `
-NotAfter (Get-Date).AddYears(3)
# Експортувати у PFX
$password = ConvertTo-SecureString -String "YourPassword" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath "MyCert.pfx" -Password $password
# Додати у Trusted Root (щоб Windows довіряв)
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root","LocalMachine")
$store.Open("ReadWrite")
$store.Add($cert)
$store.Close()
Для production (купити сертифікат):
Популярні Certificate Authorities:
- DigiCert — $200-500/рік, найбільш довірений
- Sectigo (Comodo) — $150-300/рік
- GlobalSign — $200-400/рік
- SSL.com — $100-200/рік
Для Microsoft Store сертифікат не потрібен — Store підписує автоматично.
Підпис .exe файлу
# Через signtool (входить у Windows SDK)
signtool sign /f MyCert.pfx /p YourPassword /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 MyWPFApp.exe
# Параметри:
# /f - шлях до PFX сертифікату
# /p - пароль сертифікату
# /fd - hash algorithm (SHA256 рекомендований)
# /tr - timestamp server (важливо!)
# /td - timestamp digest algorithm
Важливість timestamp:
Timestamp сервер додає мітку часу до підпису. Це дозволяє підпису залишатися валідним навіть після закінчення терміну дії сертифікату. Без timestamp підпис стане невалідним після expiration сертифікату.
Підпис MSI інсталятора
signtool sign /f MyCert.pfx /p YourPassword /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 MyWPFApp.msi
Підпис MSIX пакету
signtool sign /f MyCert.pfx /p YourPassword /fd SHA256 MyWPFApp.msix
Перевірка підпису
# Перевірити підпис
signtool verify /pa MyWPFApp.exe
# Детальна інформація
signtool verify /pa /v MyWPFApp.exe
Автоматизація підпису у CI/CD
GitHub Actions:
name: Build and Sign
on:
push:
branches: [ main ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet publish -c Release -r win-x64 --self-contained false
- name: Decode certificate
run: |
$bytes = [Convert]::FromBase64String("${{ secrets.CERTIFICATE_BASE64 }}")
[IO.File]::WriteAllBytes("cert.pfx", $bytes)
- name: Sign executable
run: |
& "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" sign `
/f cert.pfx `
/p "${{ secrets.CERTIFICATE_PASSWORD }}" `
/fd SHA256 `
/tr http://timestamp.digicert.com `
/td SHA256 `
./bin/Release/net8.0-windows/win-x64/publish/MyWPFApp.exe
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: signed-app
path: ./bin/Release/net8.0-windows/win-x64/publish/
Зберігання сертифікату у GitHub Secrets:
# Конвертувати PFX у Base64
$bytes = [System.IO.File]::ReadAllBytes("MyCert.pfx")
$base64 = [System.Convert]::ToBase64String($bytes)
$base64 | Out-File cert_base64.txt
Додайте у GitHub Secrets:
CERTIFICATE_BASE64— вміст cert_base64.txtCERTIFICATE_PASSWORD— пароль сертифікату
- Немає SmartScreen попереджень з першого дня
- Вища довіра користувачів
- Вимагає більш строгої верифікації identity
- Дорожче ($300-600/рік)
- Вимагає hardware token (USB ключ)
CI/CD Pipeline для WPF
Автоматизація збірки, тестування та deployment через CI/CD pipeline.
GitHub Actions — повний workflow
name: WPF CI/CD
on:
push:
branches: [ main, develop ]
tags:
- 'v*'
pull_request:
branches: [ main ]
env:
DOTNET_VERSION: '8.0.x'
PROJECT_PATH: './src/MyWPFApp/MyWPFApp.csproj'
SOLUTION_PATH: './MyWPFApp.sln'
jobs:
build-and-test:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Для GitVersion
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0
with:
versionSpec: '5.x'
- name: Determine Version
id: gitversion
uses: gittools/actions/gitversion/execute@v0
- name: Restore dependencies
run: dotnet restore ${{ env.SOLUTION_PATH }}
- name: Build
run: dotnet build ${{ env.SOLUTION_PATH }} -c Release --no-restore /p:Version=${{ steps.gitversion.outputs.semVer }}
- name: Run tests
run: dotnet test ${{ env.SOLUTION_PATH }} -c Release --no-build --verbosity normal
- name: Publish (Framework-dependent)
run: dotnet publish ${{ env.PROJECT_PATH }} -c Release -r win-x64 --self-contained false -o ./publish/framework-dependent
- name: Publish (Self-contained)
run: dotnet publish ${{ env.PROJECT_PATH }} -c Release -r win-x64 --self-contained true -o ./publish/self-contained
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: wpf-app-${{ steps.gitversion.outputs.semVer }}
path: ./publish/
create-installer:
needs: build-and-test
runs-on: windows-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: wpf-app-${{ needs.build-and-test.outputs.version }}
path: ./publish/
- name: Setup WiX
run: dotnet tool install --global wix
- name: Build MSI
run: wix build ./installer/Product.wxs -o ./installer/MyWPFApp.msi
- name: Decode certificate
run: |
$bytes = [Convert]::FromBase64String("${{ secrets.CERTIFICATE_BASE64 }}")
[IO.File]::WriteAllBytes("cert.pfx", $bytes)
- name: Sign MSI
run: |
& "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" sign `
/f cert.pfx `
/p "${{ secrets.CERTIFICATE_PASSWORD }}" `
/fd SHA256 `
/tr http://timestamp.digicert.com `
/td SHA256 `
./installer/MyWPFApp.msi
- name: Create Squirrel release
run: |
dotnet tool install --global Clowd.Squirrel
squirrel pack --packId MyWPFApp --packVersion ${{ needs.build-and-test.outputs.version }} --packDirectory ./publish/framework-dependent
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
files: |
./installer/MyWPFApp.msi
./releases/Setup.exe
./releases/RELEASES
./releases/*.nupkg
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to update server
run: |
# Завантажити Squirrel releases на сервер
scp -r ./releases/* user@myserver.com:/var/www/myapp/releases/
Azure DevOps Pipeline
trigger:
branches:
include:
- main
- develop
tags:
include:
- v*
pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- task: UseDotNet@2
inputs:
version: '8.0.x'
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild@1
inputs:
solution: '$(solution)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: VSTest@2
inputs:
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
publishWebProjects: false
projects: '**/MyWPFApp.csproj'
arguments: '-c Release -r win-x64 --self-contained false -o $(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
- stage: Deploy
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
jobs:
- job: DeployJob
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'drop'
downloadPath: '$(System.ArtifactsDirectory)'
- task: PowerShell@2
displayName: 'Create Installer'
inputs:
targetType: 'inline'
script: |
# WiX build script
wix build Product.wxs -o MyWPFApp.msi
- task: AzureFileCopy@4
displayName: 'Deploy to Azure Storage'
inputs:
SourcePath: '$(System.ArtifactsDirectory)/drop/*'
azureSubscription: 'MyAzureSubscription'
Destination: 'AzureBlob'
storage: 'myappupdates'
ContainerName: 'releases'
Best Practices для WPF Deployment
Підсумуємо найкращі практики для розгортання WPF додатків:
1. Вибір технології deployment
Для простих додатків (indie, open-source):
- Squirrel для auto-updates
- ZIP portable версія для advanced користувачів
- GitHub Releases для розповсюдження
Для commercial додатків:
- MSI інсталятор (через WiX або Advanced Installer)
- Squirrel для auto-updates
- Code signing (EV для великих проєктів)
- Власний update server
Для enterprise додатків:
- MSI інсталятор з silent install підтримкою
- Group Policy deployment
- SCCM/Intune integration
- Немає auto-updates (IT контролює оновлення)
Для Microsoft Store:
- MSIX пакет
- Вбудовані auto-updates через Store
- Обов'язковий code signing (Store підписує автоматично)
2. Версіонування
Використовуйте Semantic Versioning (SemVer):
MAJOR.MINOR.PATCH(наприклад, 1.2.3)- MAJOR — breaking changes
- MINOR — нові features (backward compatible)
- PATCH — bug fixes
Автоматизуйте через GitVersion:
# GitVersion.yml
mode: Mainline
branches:
main:
tag: ''
develop:
tag: 'beta'
feature:
tag: 'alpha'
3. Тестування перед release
Обов'язкові перевірки:
- Unit tests проходять
- Integration tests проходять
- Додаток запускається на чистій Windows VM
- Інсталятор працює (install/uninstall)
- Auto-updates працюють
- Code signing валідний
- Немає SmartScreen попереджень (для EV сертифікатів)
Тестові середовища:
- Windows 10 (мінімальна підтримувана версія)
- Windows 11 (остання версія)
- Чиста VM без встановленого .NET (для framework-dependent)
4. Документація для користувачів
System requirements:
System Requirements:
- Windows 10 version 1809 or higher
- .NET 8 Desktop Runtime (installer will install automatically)
- 100 MB free disk space
- 4 GB RAM recommended
Installation guide:
- Покрокова інструкція з скріншотами
- Troubleshooting для поширених проблем
- Контакти для підтримки
5. Моніторинг та аналітика
Додайте телеметрію для відстеження:
- Кількість установок
- Версії у використанні
- Crashes та errors
- Update success rate
Популярні рішення:
- Application Insights (Azure)
- Sentry (error tracking)
- Google Analytics (usage tracking)
- Інформуйте користувачів у Privacy Policy
- Надайте можливість opt-out
- Не збирайте персональні дані без згоди
- Дотримуйтесь GDPR (для EU користувачів)
Резюме
Розгортання WPF додатків — це багатогранний процес, який вимагає ретельного планування та вибору правильних інструментів. Основні висновки:
Технології deployment:
- ClickOnce — найпростіший, але застарілий. Підходить для простих internal додатків.
- MSIX — сучасний стандарт Microsoft. Обов'язковий для Store, рекомендований для Windows 10/11.
- MSI — традиційний формат. Найкраща підтримка у enterprise, працює на всіх версіях Windows.
- Squirrel — найкращий вибір для auto-updates у desktop додатках. Повний контроль, delta updates, швидкий.
Code signing — критично важливий для довіри користувачів. EV сертифікат рекомендований для commercial додатків для уникнення SmartScreen попереджень.
Auto-updates — обов'язкова функція для сучасних додатків. Користувачі очікують автоматичні оновлення без ручного завантаження нових версій.
CI/CD — автоматизація збірки, тестування та deployment економить час та зменшує помилки. GitHub Actions та Azure DevOps надають потужні інструменти для WPF проєктів.
Правильне розгортання — це не просто технічна задача, а важлива частина user experience, яка впливає на сприйняття вашого додатку користувачами.
- Deployment — розгортання, процес підготовки та встановлення додатку
- Self-contained — автономний додаток з включеним .NET Runtime
- Framework-dependent — додаток, який вимагає встановленого .NET Runtime
- ClickOnce — технологія Microsoft для web-based deployment
- MSIX — сучасний формат пакування для Windows 10/11
- MSI — Windows Installer, традиційний формат інсталяторів
- WiX — Windows Installer XML, інструмент для створення MSI
- Squirrel — фреймворк для автоматичних оновлень desktop додатків
- Code Signing — цифровий підпис для підтвердження автентичності
- SmartScreen — система захисту Windows від невідомих додатків
- EV Certificate — Extended Validation сертифікат з вищою довірою
- Bootstrapper — інсталятор, який встановлює prerequisites перед основним додатком
Додаткові ресурси
Попередня стаття: Пакування та розгортання Avalonia додатків — розгортання кросплатформних Avalonia додатків на Windows, Linux та macOS.
Пакування та розгортання Avalonia додатків
Підготовка Avalonia-додатку для розповсюдження: dotnet publish, self-contained vs framework-dependent, trimming, platform-specific packaging, auto-updates та CI/CD
Основи комп'ютерних мереж
Еволюція мереж від ARPANET до сучасного Інтернету, типи мереж, мережеве обладнання та пакетна комутація