Tester l'existence d'un fichier avec les API Win32 pour WinRT
C’est un sujet qui revient périodiquement quand vous développez une application pour Metro : comment savoir si un fichier existe déjà ?
Malheureusement, les API de WinRT ne proposent pas par défaut de méthode comme FileExists du Framework 1.0 de .NET ! Pour l’instant, je n’ai trouvé que deux possibilités.
Par itération
On effectue une itération sur l’ensemble des fichiers du StorageFolder et dès qu’on a trouvé le fichier recherché, on retourne true, sinon on retourne false. Cette méthode a été exposée par David Catuhe sur son blog et se résume ainsi :
public static class StorageFolderExtensions { public static async Task<bool> FileExistsAsync(this StorageFolder folder, string name) { var files = await folder.GetFilesAsync(); return files.Any(f => f.Name == name); } }
L’inconvénient de cette méthode est que si votre dossier contient de nombreux fichiers, elle est “lente”.
La méthode brutale
Cette dernière essaye d’obtenir une référence vers le fichier et si il n’existe pas, une exception est déclenchée. Il suffit donc de mettre tout cela dans un try/catch :
public static class StorageFolderExtensions { public static async Task<bool> FileExistsAsync(this StorageFolder folder, string name) { try { await folder.GetFileAsync(name); return true; } catch { return false; } } }
L’intérêt est que cette méthode est plus rapide mais qu’elle est basée sur le déclenchement d’une exception, ce qui n’est pas du meilleur effet !
Il y a tout de même une autre solution : l’utilisation des API Win32
La méthode Win32
Depuis que je fais du .NET, j’avoue que je n’ai jamais plus utilisé les API Win32 comme on le faisait couramment avec Visual Basic 4-6. C’est donc avec une larme à l’oeil (souvenir, souvenir) que je me suis replongé dedans. Avec Metro, vous avez accès à une liste d’API Win32 triés sur le volet par Microsoft. Parmi celles-çi, il y en a une qui nous intéresse plus particulièrement ici : GetFieAttributesEx.
Nous allons donc créer une classe NativeOperations ou nous allons déclarer l’API et les structures qui sont nécessaires :
internal class NativeOperations { // ReSharper disable InconsistentNaming /// <summary> /// La déclaration de l'API GetFileAttributesEx /// </summary> /// <param name="lpFileName">Le nom du fichier recherché</param> /// <param name="fInfoLevelId">Quelles types d'info on recherche</param> /// <param name="lpFileInformation">Structure contenant les informations une fois l'API appelée</param> /// <returns></returns> [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetFileAttributesEx( string lpFileName, GET_FILEEX_INFO_LEVELS fInfoLevelId, out WIN32_FILE_ATTRIBUTE_DATA lpFileInformation); /// <summary> /// Type d'infos recherchées /// </summary> public enum GET_FILEEX_INFO_LEVELS { /// <summary> /// C'est la seule valeur autorisée avec WinRT /// </summary> GetFileExInfoStandard, /// <summary> /// Ne fonctionne pas sous WinRT => marqué Obsolete /// </summary> [Obsolete] GetFileExMaxInfoLevel } /// <summary> /// Structure contenant les informations sur le fichier /// </summary> [StructLayout(LayoutKind.Sequential)] public struct WIN32_FILE_ATTRIBUTE_DATA { public FileAttributes dwFileAttributes; public FILETIME ftCreationTime; public FILETIME ftLastAccessTime; public FILETIME ftLastWriteTime; public uint nFileSizeHigh; public uint nFileSizeLow; } // quelques valeurs d'erreurs succeptibles d'arriver private const int ERROR_FILE_NOT_FOUND = 2; private const int ERROR_PATH_NOT_FOUND = 3; private const int ERROR_ACCESS_DENIED = 5;
Maintenant, il ne nous reste plus qu’à utiliser cette API :
public static bool FileExist(string fileName) { WIN32_FILE_ATTRIBUTE_DATA fileData; if (GetFileAttributesEx(fileName, GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, out fileData)) return true; // récupération de la dernière erreur (in the same thread of course ;-)) var lastError = Marshal.GetLastWin32Error(); if (lastError == ERROR_FILE_NOT_FOUND || lastError == ERROR_PATH_NOT_FOUND) return false; // si c'est pas un fichier non trouvé, on lance une exception if (lastError == ERROR_ACCESS_DENIED) throw new SecurityAccessDeniedException("Accès interdit"); throw new InvalidOperationException(string.Format("Erreur pendant l'accès au fichier {0}, code {1}", fileName, lastError)); }
Et ca marche, sans exception, sans itération, et c’est très rapide.
Limitations de cette démarche
Il y a une grosse limitation à cette démarche : elle ne fonctionne qu’avec le local storage ! Même si vous déclarez que vous avez le droit d’accéder au dossier des Images & co, il refusera d’y accéder (AccessDeniedException). Donc cela va très bien si vous recherchez des fichiers dans votre local Storage, mais vous pouvez la mettre à la poubelle si vous voulez rechercher dans un autre dossier (sniff, sniff).
En revanche, côté performances, il n’y a pas photo. J’ai effectué un test (IMPORTANT : en mode Release) sur un répertoire contenant plus de 600 fichiers. J’effectue une recherche d’existence de chacun des fichiers puis sur des noms de fichiers dont je sais pertinemment qu’ils n’existent pas. Résultat : je vais entre 5 et 10 fois plus vite avec les API Win32.
Il n’y a donc pas photo : si vous devez chercher régulièrement dans votre local Storage, utilisez cette méthode. En revanche, elle ne vous sera d’aucunes utilités ailleurs. Dommage !
En espérant que la version finale des API WinRT comportera une méthode optimisée…