Створення 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), та мати можливість чисто видалити додаток коли він більше не потрібен. Давайте розберемося, як це зробити правильно.
Перш ніж створювати інсталятори, потрібно правильно зібрати додаток. WPF додатки збираються через dotnet publish або MSBuild.
dotnet publish -c Release
Ця команда:
bin/Release/net8.0-windows/publish/Framework-dependent (за замовчуванням):
dotnet publish -c Release --self-contained false
Переваги:
Недоліки:
Self-contained (включає runtime):
dotnet publish -c Release --self-contained true -r win-x64
Переваги:
Недоліки:
Для 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!
Упакування всього додатку в один .exe файл:
dotnet publish -c Release --self-contained true -r win-x64 -p:PublishSingleFile=true
Переваги:
Недоліки:
Рекомендація: Для WPF краще використовувати звичайний publish без single-file, а потім створити інсталятор.
ClickOnce — це технологія Microsoft для web-based deployment з автоматичними оновленнями. Користувач відкриває URL, клікає "Install", і додаток встановлюється та автоматично оновлюється.
Крок 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 та встановлює
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;
}
}
MSIX — це сучасний формат пакування для Windows, який замінює AppX та є рекомендованим способом розповсюдження додатків через Microsoft Store та enterprise deployment.
Спосіб 1: Visual Studio (найпростіший)
Спосіб 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
<?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 підтримує автоматичні оновлення через:
1. Microsoft 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)
MSI (Microsoft Installer) — це традиційний формат інсталяторів для Windows, який існує з Windows 2000. Хоча MSIX є сучаснішим, MSI все ще широко використовується, особливо в enterprise середовищі.
/quiet параметр для автоматизації.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
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>
Для автоматичної установки .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, який:
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
Це корисно для:
Squirrel — це open-source фреймворк для автоматичних оновлень desktop додатків. На відміну від ClickOnce та MSIX, Squirrel дає повний контроль над процесом оновлення.
dotnet add package Squirrel.Windows
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}");
}
}
}
Крок 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
Code signing — це процес цифрового підпису вашого додатку для підтвердження автентичності та цілісності. Це критично важливо для довіри користувачів та уникнення попереджень Windows SmartScreen.
Без code signing:
З 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:
Для Microsoft Store сертифікат не потрібен — Store підписує автоматично.
# Через 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 сертифікату.
signtool sign /f MyCert.pfx /p YourPassword /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 MyWPFApp.msi
signtool sign /f MyCert.pfx /p YourPassword /fd SHA256 MyWPFApp.msix
# Перевірити підпис
signtool verify /pa MyWPFApp.exe
# Детальна інформація
signtool verify /pa /v MyWPFApp.exe
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 — пароль сертифікатуАвтоматизація збірки, тестування та deployment через CI/CD pipeline.
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/
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'
Підсумуємо найкращі практики для розгортання WPF додатків:
Для простих додатків (indie, open-source):
Для commercial додатків:
Для enterprise додатків:
Для Microsoft Store:
Використовуйте Semantic Versioning (SemVer):
MAJOR.MINOR.PATCH (наприклад, 1.2.3)Автоматизуйте через GitVersion:
# GitVersion.yml
mode: Mainline
branches:
main:
tag: ''
develop:
tag: 'beta'
feature:
tag: 'alpha'
Обов'язкові перевірки:
Тестові середовища:
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:
Додайте телеметрію для відстеження:
Популярні рішення:
Розгортання WPF додатків — це багатогранний процес, який вимагає ретельного планування та вибору правильних інструментів. Основні висновки:
Технології deployment:
Code signing — критично важливий для довіри користувачів. EV сертифікат рекомендований для commercial додатків для уникнення SmartScreen попереджень.
Auto-updates — обов'язкова функція для сучасних додатків. Користувачі очікують автоматичні оновлення без ручного завантаження нових версій.
CI/CD — автоматизація збірки, тестування та deployment економить час та зменшує помилки. GitHub Actions та Azure DevOps надають потужні інструменти для WPF проєктів.
Правильне розгортання — це не просто технічна задача, а важлива частина user experience, яка впливає на сприйняття вашого додатку користувачами.
Попередня стаття: Пакування та розгортання Avalonia додатків — розгортання кросплатформних Avalonia додатків на Windows, Linux та macOS.
Пакування та розгортання Avalonia додатків
Підготовка Avalonia-додатку для розповсюдження: dotnet publish, self-contained vs framework-dependent, trimming, platform-specific packaging, auto-updates та CI/CD
C# & .NET: The Ultimate Roadmap
Цей план є детальним путівником по екосистемі C#. Він побудований за принципом Stack-Centric ("Від Ядра до Сфер") і розбитий на атомарні теми для послідовного вивчення.