Ça y est, vous avez fini votre application d’entreprise Windows Store. Elle est pas encore parfaite mais cela s’arrangera au prochaine mise à jour.

Les mises à jours ! Vous y avez pensé ? Mais comment être sûr que les utilisateurs utilisent bien la dernière version de votre programme ?

Je vous propose ici une solution qui a comme seule contrainte d’utiliser un service WCF, donc qui ne fonctionnera que pour une application connectée, mais après tout, comment vérifier l’existence d’une mise à jour en mode déconnecté ?

Les prérequis

Le fonctionnement de ce système nécessite d’avoir un minimum la main sur le serveur hébergeant le WCF, à savoir :

  • Pouvoir y copier des fichiers dans un dossier spécifique et qui sera partagé en lecture seule.
    • Ces fichiers seront : le package Appx en cours et un fichier Bat d’installation
  • Le service Web devra également avoir le droit de lire dans ce dossier.
  • Enfin, un autre site Web devra être créé – sur ce serveur ou un autre, peut importe – afin d’héberger le script de lancement de la mise à jour.

L’autre prérequis essentiel est que le package de l’application doit impérativement être signé.

L’approche

En partant sur une application basée sur un modèle MVVM la logique de fonctionnement est la suivante :

J’ai une Loading Page qui mimique le Splash Screen (assez classique comme principe) à laquelle est associée un ViewModel.

Lors de l’initialisation de ce ViewModel, une requête vers le service WCF va récupérer la version en cours du package disponible à l’installation et modifie une propriété booléenne indiquant si une mise à jour est disponible (en comparant la version retournée avec celle du package en cours d’exécution).

Le code behind de la vue vérifie cette propriété avant toute autre action et, si une mise à jour est nécessaire, invite l’utilisateur à la faire avant de continuer l’initialisation de l’application.

Si l’utilisateur décide de faire l’update, une Uri est appelée pour démarrer le processus de mise à jour via un batch et l’application est fermée.

Lorsqu’une mise à jour est disponible, un MessageDialog est affiché pour permettre à l’utilisateur de faire la mise à jour

La configuration du serveur

Pour l’exemple, nous imaginerons la configuration suivante :

    • un service WCF disponible à l’adresse http://contoso/SuperApp/SuperWCF.svc
    • Sur ce même serveur, un dossier D:\SuperApp\Package qui contiendra le package en cours (partagé en tant que \\contoso\SuperAppPackage\)
    • Un autre site web http://contoso/SuperAppUpdate qui contiendra un fichier Bat au contenu suivant
 1: start "Updating SuperApp" /WAIT /B \\contoso\SuperAppPackage\add-ModernApplication.bat
 2: exit

et le fichier Web.config

 1: <?xml version="1.0" encoding="UTF-8"?>
 2: <configuration>
 3:     <system.webServer>
 4:         <staticContent>
 5:             <mimeMap fileExtension=".bat" mimeType="application/octet-stream" />
 6:         </staticContent>
 7:     </system.webServer>
 8: </configuration>

Le code de l’application

LoadingPage.Xaml.cs

 1: public sealed partial class LoadingPage : LayoutAwarePage
 2: {
 3:     public LoadingPageViewModel ViewModel
 4:     {
 5:         get { return this.DataContext as LoadingPageViewModel; }
 6:         set { this.DataContext = value; }
 7:     }
 8:  
 9:     public LoadingPage()
 10:     {
 11:         this.InitializeComponent();
 12:  
 13:         //Initialisation du ViewModel
 14:         var viewModel = new LoadingPageViewModel(ViewService.Service);
 15:         this.ViewModel = viewModel;
 16:  
 17:         //Verification de la disponibilité d'une mise à jour
 18:         var isOutDatedTask = CheckOutdatedVersion();
 19:         isOutDatedTask.Wait();
 20:         var isOutDated = isOutDatedTask.Result;
 21:     }
 22:  
 23:     private async Task<bool> CheckOutdatedVersion()
 24:     {
 25:         if (this.ViewModel.IsOutdated)
 26:         {
 27:             bool? requestUpdate = null;
 28:             var message = new MessageDialog(@"Super App n'est pas à jour. Voulez-vous effectuer la mise à jour ?", "Super App")
 29:                 {
 30:                     Commands =
 31:                 {
 32:                     new UICommand(@"Mettre à jour", cmd => requestUpdate = true),
 33:                     new UICommand(@"Quitter Super App", cmd => requestUpdate = false)
 34:                 },
 35:                     DefaultCommandIndex = 0,
 36:                     CancelCommandIndex = 1
 37:                 };
 38:  
 39:             await message.ShowAsync();
 40:  
 41:             if (!requestUpdate.HasValue || !requestUpdate.Value)
 42:                 Application.Current.Exit();
 43:             else
 44:             {
 45:                 await Windows.System.Launcher.LaunchUriAsync(new Uri(new Uri(@"http://contoso/SuperAppUpdate/"), "Update.bat"));
 46:                 Application.Current.Exit();
 47:             }
 48:             return true;
 49:         }
 50:         return false;
 51:     }
 52: }

LoadingPaveViewModel.cs

 1: public class LoadingPageViewModel : BindableBase
 2: {
 3:     private bool _isOutdated;
 4:     public bool IsOutdated
 5:     {
 6:         get { return _isOutdated; }
 7:         set { SetProperty(ref _isOutdated, value); }
 8:     }
 9:  
 10:     public LoadingPageViewModel(IViewService viewService)
 11:         : base(viewService)
 12:     {
 13:         CheckVersion();
 14:     
 15:         if (IsOutdated)
 16:             return;
 17:     }
 18:  
 19:     private void CheckVersion()
 20:     {
 21:         try
 22:         {
 23:             //Appel du WCF pour récupérer la version du dernier package disponible
 24:             var lastVersion = WCFProxy.SuperWCFClient.GetCurrentVersion();
 25:             //Récupération de la version du package en cours d'éxecution
 26:             var packageVersion = Windows.ApplicationModel.Package.Current.Id.Version;
 27:             //Conversion du type PackageVersion en Version
 28:             var currentVersion =
 29:                 new Version(string.Format("{0}.{1}.{2}.{3}", packageVersion.Major, packageVersion.Minor,
 30:                                           packageVersion.Build, packageVersion.Revision));
 31:             //Comparaison des versions
 32:             IsOutdated = currentVersion.CompareTo(lastVersion) < 0;
 33:         }
 34:         catch (Exception)
 35:         {
 36:             //Si le WCF n'est pas accessible, on considère que la version est à jour
 37:             IsOutdated = false;
 38:         }
 39:     }
 40: }

SuperWCF.svc.cs

 1: public class SuperWCF
 2: {
 3:  
 4:     private const string SuperAppPackageFolder = @"D:\SuperApp\Package\";
 5:     private const string SuperAppPackageNameRegEx = @"SuperApp.*_(\d*[.]\d*[.]\d*[.]\d*).*";
 6:  
 7:     /// <summary>
 8:     /// Gets the current SuperApp package version.
 9:     /// </summary>
 10:     /// <returns></returns>
 11:     /// <exception cref="System.IO.FileNotFoundException">SuperApp Package not found</exception>
 12:     public Version GetCurrentVersion()
 13:     {
 14:         Version returnVersion;
 15:  
 16:         try
 17:         {
 18:             var folder = new DirectoryInfo(SuperAppPackageFolder);
 19:             var files = new List<FileInfo>(folder.GetFiles("*.appx"));
 20:             if (files.Count <= 0)
 21:                 throw new FileNotFoundException("Kymu Package not found");
 22:  
 23:             var packageFile = files.OrderByDescending(f => f.Name).First();
 24:             returnVersion = ExtractVersionNumberFromPackageName(packageFile.Name);
 25:         }
 26:         catch
 27:         {
 28:             returnVersion = new Version(1, 0, 0, 0);
 29:         }
 30:         return returnVersion;
 31:     }
 32:  
 33:     /// <summary>
 34:     /// Extracts the version number from the name of the SuperApp package.
 35:     /// </summary>
 36:     /// <param name="packageName">Name of the package.</param>
 37:     /// <returns></returns>
 38:     private static Version ExtractVersionNumberFromPackageName(string packageName)
 39:     {
 40:         var match = Regex.Match(packageName, SuperAppPackageNameRegEx, RegexOptions.IgnoreCase);
 41:         Version returnValue;
 42:         if (!match.Success || match.Groups.Count < 2 || !Version.TryParse(match.Groups[1].Value, out returnValue))
 43:             returnValue = new Version(1, 0, 0, 0);
 44:         return returnValue;
 45:     }
 46:  
 47: }

Le Batch d’installation

Voici le fichier batch qui réalise au l’installation et qui peut fonctionner depuis un partage réseau

Add-ModernApplication.bat

 1: @ECHO OFF
 2:  
 3: REM #######################################################################
 4: REM # Add-ModernApplication.bat
 5: REM # <summary>
 6: REM # Add-ModernApplication.bat is a powershell.exe script designed to install
 7: REM # the Microsoft Corporate Appx packages created by Visual Studio. 
 8: REM #
 9: REM # The target folder will contain the .appx application file,
 10: REM # the Add-ModernApplication.bat script, plus a .\Dependencies\ folder 
 11: REM # containing all the framework packages used by the application, if needed
 12: REM # When executed from a local directory, the Add-ModernApplication.bat script
 13: REM # simplifies installing the AppX package on a new computer by
 14: REM # automating the following functions.
 15: REM #
 16: REM #
 17: REM # 1. Checks for the Win8 or Win8.1 OS client.
 18: REM # 2. Checks for the client to be volume licensed
 19: REM # 3. Checks for the build to be at the minimum level
 20: REM # 4. If a server SKU, checks for the Desktop Experience Feature
 21: REM #
 22: REM # 4. Installs all the framework packages contained in
 23: REM # .\Dependencies
 24: REM #
 25: REM # 5. Installs the application package .appx file.
 26: REM # 
 27: REM # The Assumption is made that the client satisfies all the requirements 
 28: REM # for Enterprise side-loading of AppX packages and that the Package is signed
 29:  
 30: REM #######################################################################
 31:  
 32: set W8RTM=9200
 33: set W81RTM=9600
 34: set RP=8400
 35:  
 36: REM Check for Windows 8 OS and build
 37: for /f "delims=[] tokens=2" %%i in ('ver') do set MyVer=%%i
 38: for /f "tokens=2" %%i in ('echo %MyVer%') do set MyVer=%%i
 39: for /f "tokens=1,2,3 delims=." %%i in ('echo %MyVer%') do (
 40:     set OSMajorVer=%%i
 41:     set OSMinorVer=%%j
 42:     set OSBuild=%%k
 43: )
 44:  
 45: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "`n`nThe Script identified this machine as Windows Version "%OSMajorVer%"."%OSMinorVer%", Build Number " %OSBuild%"`n" -foregroundcolor "green" 
 46: REM echo The Script identified this machines as %OSMajorVer%.%OSMinorVer%, Build Number %OSBuild%
 47: REM # echo Found OS Minor Version: %OSMinorVer%
 48: REM # Found OS Build number: %OSBuild%
 49:  
 50:  
 51: REM Get SKU of client to test for Volume licensing
 52:  
 53: REM for /f "delims= tokens=1" %%i in ('%windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command "(get-wmiobject win32_operatingsystem).OperatingSystemSKU"') do set MySku=%%i
 54: for /f "delims= tokens=1" %%i in ('%windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command "(get-wmiobject win32_operatingsystem).OperatingSystemSKU"') do set MySku=%%i
 55:  
 56: If %OSMajorVer% EQU 6 if %OSMinorVer% EQU 2 goto Win8VLtest
 57: If %OSMajorVer% EQU 6 if %OSMinorVer% EQU 3 goto Win8VLtest
 58:  
 59: REM Version failed, not Windows8
 60: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "ERROR: Windows 8 Modern Applications are only supported on Windows 8." -foregroundcolor "red"
 61: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program `n" -foregroundcolor "red"
 62: goto end
 63:  
 64:  
 65: :Win8VLtest
 66:  
 67: REM Checking for volume licensed media 
 68:  
 69:  
 70: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "`n`nThe SKU "%MySku%""
 71:  
 72: if %MySku% EQU 4 goto Win8RTMtest
 73: if %MySku% EQU 27 goto Win8RTMtest
 74: if %MySku% EQU 48 goto Win8RTMtest
 75: if %MySku% EQU 77 goto Win8RTMtest
 76: if %MySku% EQU 80 goto Win8RTMtest
 77: if %MySku% EQU 97 goto Win8RTMtest
 78: if %MySku% EQU 8 goto Win8BuildServer
 79: if %MySku% EQU 10 goto Win8BuildServer
 80: if %MySku% EQU 42 goto Win8BuildServer
 81: if %MySku% EQU 38 goto Win8BuildServer
 82:  
 83: REM not VL media, Failure
 84: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "ERROR: Sideloaded Windows 8 Modern applications may only be installed on Volume Licensed OS SKUs of Windows 8." -foregroundcolor "red" 
 85: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program" -foregroundcolor "red" 
 86: goto end
 87:  
 88: :Win8BuildServer
 89:  
 90: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "This machine has been identifed as a Server OS."  -foregroundcolor "green" 
 91:  
 92: for /f "delims= tokens=1" %%i in  ('%windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command "(Get-WindowsFeature desktop-experience).Installstate"') do Set DesktopEx=%%i
 93:  
 94: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "This Server sku appears to have the Desktop Experience "%DesktopEx%", which is a requirement for side-loading of Modern Apps"  -foregroundcolor "green" 
 95: If %DesktopEx% EQU Installed goto Win8RTMtest
 96:  
 97: REM Server does not have Desktop-Experience implemented, Failure
 98: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "ERROR: This machine appears to be a valid server SKU, but you need to implement the Desktop-Experience feature, first." -foregroundcolor "red"
 99: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program" -foregroundcolor "red" 
 100: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Open up elevated Powershell and run" -foregroundcolor "red" 
 101: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Import-Module ServerManager , then , Install-WindowsFeature Desktop-Experience" -foregroundcolor "red" 
 102: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "You will then need to reboot your system" -foregroundcolor "red" 
 103: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program" -foregroundcolor "red" 
 104: goto end
 105:  
 106:  
 107: :Win8RTMtest
 108:  
 109: if %OSBuild% EQU %W8RTM% goto  Win8Pass
 110: if %OSBuild% EQU %W81RTM% goto  Win8Pass
 111: REM if %OSBuild% EQU %RP% goto Win8Pass
 112:  
 113:  
 114: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "ERROR: This program is only intended on Windows 8 RTM (6.2.9200) or Windows 8.1 RTM (6.2.9600)." -foregroundcolor "red"
 115: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "Ending Program `n" -foregroundcolor "red"
 116: goto end
 117:  
 118: :Win8Pass
 119:  
 120: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "This machine appears to satisfy the requirements needed to install this Modern application. `n Proceeding to install which may take a minute. Please wait... `n" -foregroundcolor "green"
 121: pushd %~dp0
 122: SET Test-Path=%~dp0
 123:  
 124: REM Proceed to add in the AppX package found in this same directory
 125: REM Only one AppX package should exist in this same directory
 126:  
 127: ECHO Using %windir%\System32\WindowsPowerShell\v1.0\powershell.exe Cmdlets to Install Appx Files
 128:  
 129:  
 130: ECHO  ^- Installing Dependency Packages...
 131: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Import-Module appx; if(Test-Path -Path .\Dependencies) { $DependencyFiles = get-childItem .\Dependencies ^| where{$_.extension -eq '.appx'} ^| foreach-object -process{$_.FullName}; if(!$DependencyFiles) {exit 1}; foreach($Dependency in $DependencyFiles){ Write-Output " Installing: $Dependency"; Add-AppxPackage $Dependency; if(!$?){exit{5}}} } else {exit 1}
 132:  
 133: set RETURN=%ERRORLEVEL%
 134: set RETURNDependency = %RETURN%
 135: IF "%RETURN%"=="1" ECHO      No Dependencies Found.
 136: IF "%RETURN%"=="5" (
 137:     %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host " ERROR: Dependent Package identified, but FAILED TO INSTALL!" -foregroundcolor "red"
 138:     goto CLEANUP
 139:     )
 140:         
 141:  
 142: ECHO  ^- Installing Windows 8 Modern Application...        
 143: %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Import-Module appx; $PackageFile = get-childItem .\ ^| where{$_.extension -eq '.appx'} ^| foreach-object -process{$_.FullName}; if(!$PackageFile){exit 6}; if($PackageFile.Count -gt 1){exit 7}; Write-Output " Installing: $PackageFile"; Add-AppxPackage $PackageFile; if(!$?) {exit 8}
 144: set RETURN=%ERRORLEVEL%
 145: IF "%RETURN%"=="0" %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host "`n SUCCESS! Find the tile for this application on the far right side of your Windows 8 Start Screen. `n" -foregroundcolor "green"
 146: IF "%RETURN%"=="6" %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host " ERROR: NO APPLICATION FOUND TO INSTALL! `n" -foregroundcolor "red"
 147: IF "%RETURN%"=="7" %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host " ERROR: MORE THAN ONE APPLICATION PACKAGE FOUND! 'n" -foregroundcolor "red"
 148: IF "%RETURN%"=="8" %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -command Write-Host " ERROR: FAILED TO INSTALL WINDOWS 8 APPLICATION! 'n" -foregroundcolor "red"
 149:    
 150: :CLEANUP
 151: popd
 152: :end
 153: pause

154: exit /b %RETURN%

Voilà !

N’hésitez pas à me solliciter si vous avez la moindre question concernant ce modèle de mise à jour automatisée