Desktop UI

Розгортання WPF застосунків

Підготовка WPF-додатку для розповсюдження на Windows: ClickOnce, MSIX, MSI інсталятори, Squirrel для auto-updates, code signing та best practices

Розгортання 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
Що обрати для WPF?Для більшості WPF додатків рекомендується framework-dependent deployment з автоматичною установкою runtime через інсталятор. Причини:
  1. Windows користувачі звикли встановлювати prerequisites
  2. Багато додатків використовують .NET, тому runtime вже встановлений
  3. Менший розмір інсталятора
  4. Автоматичні security updates
Self-contained варто обирати тільки якщо:
  • Додаток розповсюджується через 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

Простота Найпростіший спосіб deployment для WPF. Не потрібно створювати інсталятор — Visual Studio генерує все автоматично.
Auto-updates Вбудована підтримка автоматичних оновлень. Додаток перевіряє оновлення при старті та автоматично завантажує нову версію.
Web deployment Розповсюдження через web-сервер. Користувач відкриває URL та встановлює додаток одним кліком.
Безпека Додаток працює у sandbox з обмеженими правами. Не потрібен UAC elevation.

Недоліки 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;
    }
}
ClickOnce застарівMicrosoft рекомендує використовувати MSIX замість ClickOnce для нових проєктів. ClickOnce все ще підтримується, але не розвивається.Використовуйте ClickOnce тільки якщо:
  • Потрібна підтримка старих версій Windows (XP, Vista)
  • Вже є існуюча ClickOnce інфраструктура
  • Потрібен максимально простий deployment без інсталятора
Для нових проєктів розгляньте MSIX або Squirrel.

MSIX — сучасний формат пакування

MSIX — це сучасний формат пакування для Windows, який замінює AppX та є рекомендованим способом розповсюдження додатків через Microsoft Store та enterprise deployment.

Переваги MSIX

Сучасний стандарт Рекомендований Microsoft формат для Windows 10/11. Підтримка у Microsoft Store, Intune, SCCM.
Чиста установка/видалення Повна ізоляція додатку. При видаленні не залишається сміття у реєстрі та файловій системі.
Auto-updates Вбудована підтримка оновлень через Microsoft Store або власний сервер.
Безпека Обов'язковий code signing. Додаток працює у sandbox з обмеженими правами.

Недоліки MSIX

  • Потрібен Windows 10 1809+ (не працює на Windows 7/8)
  • Обов'язковий code signing (потрібен сертифікат)
  • Обмеження sandbox (не всі API доступні)
  • Складніше налаштувати ніж MSI

Створення MSIX пакету

Спосіб 1: Visual Studio (найпростіший)

  1. Додайте Windows Application Packaging Project до solution
  2. Правий клік → Add → Existing Project → виберіть WPF проєкт
  3. Налаштуйте Package.appxmanifest
  4. 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 годин.

Code Signing для MSIXMSIX вимагає обов'язкового code signing. Потрібен сертифікат:Для розробки (self-signed):
# Створити 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)
Для production (купити сертифікат):
  • DigiCert, Sectigo, GlobalSign — від $200/рік
  • Для Microsoft Store — сертифікат не потрібен (Store підписує автоматично)

MSI — традиційні інсталятори

MSI (Microsoft Installer) — це традиційний формат інсталяторів для Windows, який існує з Windows 2000. Хоча MSIX є сучаснішим, MSI все ще широко використовується, особливо в enterprise середовищі.

Переваги MSI

Enterprise підтримка Повна підтримка Group Policy, SCCM, Intune. Стандарт для корпоративного deployment.
Гнучкість Повний контроль над процесом установки: custom dialogs, registry, services, file associations.
Сумісність Працює на всіх версіях Windows від XP до 11. Не потрібен Windows 10+.
Silent install Підтримка тихої установки через /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, який:

  1. Перевірить наявність .NET 8 Runtime
  2. Якщо немає — встановить автоматично
  3. Встановить ваш додаток

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
Альтернативи WiXЯкщо WiX здається занадто складним, розгляньте:Advanced Installer (commercial, $500+):
  • GUI для створення MSI
  • Wizard-based підхід
  • Вбудована підтримка .NET detection
  • Visual Studio integration
Inno Setup (free, open-source):
  • Простіший ніж WiX
  • Script-based конфігурація
  • Створює .exe інсталятор (не MSI)
  • Популярний у open-source спільноті
NSIS (free, open-source):
  • Дуже гнучкий
  • Script-based
  • Малий розмір інсталятора
  • Складніший синтаксис

Squirrel — auto-updates для desktop додатків

Squirrel — це open-source фреймворк для автоматичних оновлень desktop додатків. На відміну від ClickOnce та MSIX, Squirrel дає повний контроль над процесом оновлення.

Переваги Squirrel

Справжні auto-updates Автоматичні оновлення у фоні без перезапуску додатку. Користувач навіть не помічає.
Повний контроль Програмний API для контролю оновлень: коли перевіряти, як показувати UI, rollback до попередньої версії.
Delta updates Завантажуються тільки змінені файли, а не весь додаток. Економія трафіку.
Швидкий старт Оптимізований для швидкого запуску додатку. Оновлення не блокують старт.

Установка 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!";
        }
    }
}
Squirrel.Windows vs Clowd.SquirrelОригінальний Squirrel.Windows не підтримується з 2020 року. Використовуйте форк Clowd.Squirrel:
dotnet add package Clowd.Squirrel
Clowd.Squirrel:
  • Активно підтримується
  • Підтримка .NET 6/7/8
  • Покращена продуктивність
  • Більше features (portable mode, custom installers)
API майже ідентичний, тому міграція проста.

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.txt
  • CERTIFICATE_PASSWORD — пароль сертифікату
EV Code Signing для SmartScreenЗвичайний code signing не гарантує відсутність SmartScreen попереджень. Windows SmartScreen використовує reputation-based систему — новий додаток буде показувати попередження навіть з валідним підписом, поки не набуде репутації (багато завантажень без скарг).EV (Extended Validation) Code Signing дає миттєву репутацію:
  • Немає SmartScreen попереджень з першого дня
  • Вища довіра користувачів
  • Вимагає більш строгої верифікації identity
  • Дорожче ($300-600/рік)
  • Вимагає hardware token (USB ключ)
Рекомендується для commercial додатків з великою аудиторією.

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 та GDPRЯкщо збираєте телеметрію:
  • Інформуйте користувачів у 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 перед основним додатком

Додаткові ресурси

Squirrel.Windows (Clowd.Squirrel)

Сучасний форк Squirrel для auto-updates у .NET додатках

WiX Toolset

Офіційний сайт WiX для створення MSI інсталяторів

MSIX Documentation

Офіційна документація Microsoft про MSIX пакування

Advanced Installer

Commercial інструмент для створення MSI з GUI

GitHub Actions for .NET

Документація GitHub Actions для .NET проєктів

Code Signing Best Practices

Microsoft документація про code signing

Попередня стаття: Пакування та розгортання Avalonia додатків — розгортання кросплатформних Avalonia додатків на Windows, Linux та macOS.