Comment “cuisiner” une application Windows 8 avec XAML et C# en une semaine–Jour 4
Comme le dit le père catuhe dans son billet concernant le Jour 4, nous pouvons considérer que c’est le dernier jour de notre série.
Vous retrouverez le code liée à cette article ici :https://aka.ms/isz0ot
Ainsi que les jours précédent :
Jour 0 (la Consumer Preview)
Jour 1 : (Consumer preview)
Jour 2 : (Release Preview)
Jour 2 Optimisé :(Release preview)
Ajouter un fichier pour gérer l’état de la collection de l’utilisateur.
Dans la page d’extension, nous allons pouvoir ajouter une barre d’application contextuelle afin de permettre à l’utilisateur
d’indiquer quelles cartes sont présentes dans sa collection :
Les nouveaux boutons de la barre d’outils sont alors les suivants :
Code Snippet
<Page.BottomAppBar> <AppBar x:Name="PageAppBar" Padding="10,0,10,0" IsSticky="{Binding DisplayElement,Converter={StaticResource BooleanToIsOpenConverter}}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="50*"/> <ColumnDefinition Width="50*"/> </Grid.ColumnDefinitions> <StackPanel x:Name="LeftCommands" Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left"> <Button x:Name="AddButton" x:Uid="AddButton" Visibility="{Binding DisplayElement, Converter={StaticResource IntToVisibilityConverter}}" HorizontalAlignment="Left" Style="{StaticResource CheckAppBarButtonStyle}" Click="AddButton_Click"/> <Button x:Name="DelButton" x:Uid="DelButton" Visibility="{Binding DisplayElement, Converter={StaticResource IntToVisibilityConverter}}" HorizontalAlignment="Left" Style="{StaticResource UnCheckAppBarButtonStyle}" Click="DelButton_Click" /> </StackPanel> <StackPanel x:Name="RightCommands" Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Right"> <Button x:Name="AddAllButton" x:Uid="AddAllButton" HorizontalAlignment="Left" Style="{StaticResource CheckAppBarButtonStyle}" Click="AddAllButton_Click" /> <Button x:Name="DelAllButton" x:Uid="DelAllButton" HorizontalAlignment="Left" Style="{StaticResource UnCheckAppBarButtonStyle}" Click="DelAllButton_Click" /> <Button x:Name="AlbumButton" x:Uid="AlbumButton" HorizontalAlignment="Left" Style="{StaticResource AlbumAppBarButtonStyle}" /> <Button x:Name="PinButton" x:Uid="PinButton" Visibility="{Binding PinUnpinSecondaryTile, Converter={StaticResource BooleanToCollapseConverter}}" HorizontalAlignment="Right" Style="{StaticResource PinAppBarButtonStyle}" Click="Pin_Click" /> <Button x:Name="UnPinButton" x:Uid="UnPinButton" Visibility="{Binding PinUnpinSecondaryTile,Converter={StaticResource BooleanToVisibilityConverter}}" HorizontalAlignment="Right" Style="{StaticResource UnpinAppBarButtonStyle}" Click="UnPin_Click" /> </StackPanel> </Grid> </AppBar> </Page.BottomAppBar>
Les commandes contextuelles AddButton et DelButton se trouvant à gauche les autres à droite.
1ere remarque : Tout d’abord, le style des boutons est défini sur un style particulier par exemple CheckAppBarButtonStyle pour le bouton AddButton.
Ce style est présent dans le fichier StandardStyles.xaml,
Code Snippet
- <Style x:Key="CheckAppBarButtonStyle" TargetType="Button" BasedOn="{StaticResource AppBarButtonStyle}">
- <Setter Property="AutomationProperties.AutomationId" Value="CheckAppBarButton"/>
- <Setter Property="AutomationProperties.Name" Value="Check"/>
- <Setter Property="Content" Value="✔"/>
- </Style>
Ce que l’on peut constater c’est que la propriété Content est remplie par le code de caractère 2714, ce qui dans la fonte Segoe UI Symbol donne ✔ et que le texte “Ajouter” est un texte qui provient de la propriété AutomationProperties.Name. Pour que ce texte soit traduit automatiquement en fonction du langage choisi dans les préférences de Windows, il faut encore une fois utiliser la propriété x:Uid du bouton, comme je l’ai indiqué dans le jour 2 de cette série.
Néanmoins, la syntaxe est légérement différente et ne s’invente pas malheureusement. Si vous retournez dans le fichier Resources.resw, il faudra ajouter la syntaxe suivante :
Name = AddButton.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name
Value = Ajouter
Les boutons contextuels sont réglés pour apparaitre en fonction de la selection à l’aide du modèle DisplayElement, qui sera renseigné à l’ouverture de l’AppBar, par le nombre d’éléments sélectionnés.
Code Snippet
- void PageAppBar_Opened(object sender, object e)
- {
- String expansionId = _vueData.CurrentExpansion.id.ToString();
- Boolean isTileExist = URZATileManager.IsSecondaryTileExists(expansionId);
- this.DefaultViewModel["PinUnpinSecondaryTile"] = isTileExist;
- this.DefaultViewModel["DisplayElement"] = CardsItemsGridView.SelectedItems.Count;
- }
et du Converter IntToVisibilityConverter attaché à sa propriété Visibility.
Code Snippet
- class IntToVisibilityConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, string language)
- {
- var count = (int)value;
- return count>0 ? Visibility.Visible : Visibility.Collapsed;
- }
- public object ConvertBack(object value, Type targetType, object parameter, string language)
- {
- return value is Visibility && (Visibility)value == Visibility.Visible;
- }
- }
Par défaut, la selection multiple du contrôle GridView n’étant pas activée, il ne faut pas oublier de le faire.
Code Snippet
- <GridView
- SelectionChanged="CardsItemsGridView_SelectionChanged"
- x:Name="CardsItemsGridView"
- SelectionMode="Multiple"
Ensuite sur l’évenement click des boutons AddButton/DelButton par exemple , nous allons tout d’abord déclencher la méthode SetUserData, qui aura juste pour rôle d’affecter à la propriété IsChecked d’un objet UrzaCard, si oui ou non elle fait partie de la collection de l’utilisateur.
Code Snippet
private async void AddButton_Click(object sender, RoutedEventArgs e) { try { SetUserData(true); await _vueData.SerializeUserDataAsync(); } catch (Exception ex) { Helper.ShowOnError(ex, UrzaResources.AppErrorMessage); } } private void SetUserData(bool adddata) { var SelectedCardsItem = CardsItemsGridView.SelectedItems; foreach (var objCard in SelectedCardsItem) { var card = (URZACard)objCard; card.IsChecked = adddata; } if (adddata == false) { foreach (var objCard in SelectedCardsItem) { CardsItemsGridView.SelectedItems.Remove((URZACard)objCard); } } if (SelectedCardsItem.Count > 0 || adddata == false) _vueData.AppliedFiltersAsync(); }
Puis ensuite nous déclenchons la sauvegarde, ou plutot la sérialisation XML de la collection de l’utilisateur. SerializeUserDataAsync()
Pour la serialisation de la collection de l’utilisateur, j’ai, pour simplifier le processus, crée de nouvelles classes sérialisables qui ne contiendront que le strict minimum
et ceci afin d’éviter de sérialiser la totalité des données contenues dans une carte.
Code Snippet
- public class URZAUserData
- {
- public int Version { get; set; }
- public List<URZAUserDataExpansion> Expansions;
- public URZAUserData()
- {
- Expansions = new List<URZAUserDataExpansion>();
- }
- }
- public class URZAUserDataCard
- {
- public int Id;
- }
- public class URZAUserDataExpansion
- {
- public int ExpansionId;
- public List<URZAUserDataCard> UserCards;
- public URZAUserDataExpansion()
- {
- UserCards = new List<URZAUserDataCard>();
- }
- }
Code Snippet
- private async Task internalSerializeUserDataAsync(URZAUserData userData)
- {
- try
- {
- await _semaphoreForUserData.WaitAsync(SEMAPHORE_MAX_WAIT);
- var folder = ApplicationData.Current.LocalFolder;
- IStorageFile storageFile = await folder.CreateFileAsync(USERDATA_FILE, CreationCollisionOption.ReplaceExisting);
- XmlSerializer x = new XmlSerializer(typeof(URZAUserData));
- using (var stream = await storageFile.OpenStreamForWriteAsync())
- {
- x.Serialize(stream, userData);
- }
- }
- finally
- {
- _semaphoreForUserData.Release();
- }
- }
En faite, dans le code livré avec ce billet, c’est un peu plus complexe que cela et je vous laisse le découvrir. Mais le principe est de tester si le fichier existe déjà, si oui de déserialiser les données qu’il contient, afin de les fusionner avec les données qui viennent d’être selectionnées. Cela se complique encore, lorsque le fichier est présent sur SkyDrive (Connexion que nous verrons plus tard), car il faudra tester la version de la collection de l’utilisateur.
Pour identifier qu’une carte est selectionnée, on va utiliser la propriété IsChecked de chaque carte afin de lui affecter un style particulier comme dans l’image suivante :
Pour se faire : on va passer dans mon exemple par un DataTemplateSelector
Code Snippet
- class CardsDataTemplateSelector:DataTemplateSelector
- {
- static DataTemplate UrzaCheckedCardsItemsGridViewItemTemplate = Application.Current.Resources["UrzaCheckedCardsItemsGridViewItemTemplate"] as DataTemplate;
- static DataTemplate UrzaCardsItemsGridViewItemTemplate = Application.Current.Resources["UrzaCardsItemsGridViewItemTemplate"] as DataTemplate;
- protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
- {
- var card = item as URZACard;
- return (card.IsChecked == true) ? UrzaCheckedCardsItemsGridViewItemTemplate : UrzaCardsItemsGridViewItemTemplate;
- }
- }
Dans le fichier Expansion.Xaml, il suffira alors d’y faire reference dans les ressources, puis d’indiquer l’ItemTemplateSelector adéquate pour le GridView.
Code Snippet
- <Page.Resources>
- <common:CardsDataTemplateSelector x:Key="cardsDataTemplateSelector"/>
- <GridView
- SelectionChanged="CardsItemsGridView_SelectionChanged"
- x:Name="CardsItemsGridView"
- SelectionMode="Multiple"
- Grid.Row="2"
- Margin="0,-3,0,0"
- Padding="116,0,40,46"
- IsItemClickEnabled="True"
- ItemsSource="{Binding Source={StaticResource groupedCardsItemsViewSource}}"
- ItemClick="cardsItemsGridView_ItemClick"
- ItemTemplateSelector="{StaticResource cardsDataTemplateSelector}">
Se connecter à SkyDrive
La collection de l’utilisateur sera à la fois sauvegardée localement, mais également sur son compte SkyDrive.
Tout d’abord, vous devez télécharger et installer le SDK Live. et le référencer dans votre projet :
Le SDK s’installant dans le répertoire : C:\Program Files (x86)\Microsoft SDKs\Live\v5.0
Ensuite il faut inscrire votre application, afin d’obtenir un Package Name, qui permettra d’identifer votre application de façon unique comme illustré sur la figure suivante . Pour cela il suffit de suivre les étapes décrites ici : https://manage.dev.live.com/build?wa=wsignin1.0
Si vous ne suivez pas cette étape, la connexion à SkyDrive ne fonctionnera pas.
Avec ce SDK, il est possible de créer son propre bouton de connexion comme décrit par David Catuhe dans son article, ou alors d’utiliser le contrôle prévu à cette effet, comme je l’ai fait dans la vue Home.XAML
Code Snippet
- <Controls:SignInButton Content="SignInButton" Grid.Column="2" HorizontalAlignment="Left"
- Margin="10,10,0,0" VerticalAlignment="Top" Width="233"
- SessionChanged="SignInButton_SessionChanged_1" Branding="Skydrive" Theme="Light" Height="41"/>
Lors de la 1ere utilisation, la boite de connexion s’affiche.
Une fois connecté j’affiche l’état de la connexion en affichant le nom de la personne connectée.
Pour contrôler ce qui se passe, j’ai abonné le bouton à l’évenement SessionChanged, dans lequel je vais initialiser les options de SkyDrive par l’intermediaire d’une classe Helper SkyDriveHelper
Code Snippet
- private async void SignInButton_SessionChanged_1(object sender, Microsoft.Live.Controls.LiveConnectSessionChangedEventArgs e)
- {
- var status = e.Status;
- if (e.Status == LiveConnectSessionStatus.Connected)
- {
- await SkyDriveHelper.InitAuthenticationAsync();
- txtNameProfile.Text = SkyDriveHelper.Profile["name"].ToString();
- }
- else
- {
- SkyDriveHelper.IsUserLogged = false;
- txtNameProfile.Text = "";
- }
- }
La méthode InitAuthenticationAsync(), va initialiser le champ d’application de l’application. Entre autre la possibilité de mettre à jour SkyDrive, car ensuite nous allons y créer un repertoire, puis y uploader un fichier. Ensuite je crée une connection client avec l’état de session, puis je télécharge le profile de la connection, me permettant d’afficher le nom de l’utilisateur connecté.
Code Snippet
- public static async TaskInitAuthenticationAsync()
- {
- if (!NetworkInterface.GetIsNetworkAvailable()) return;
- if (Session != null) return;
- _authenticationClient = new LiveAuthClient();
- List<String> scopes = new List<String>() { "wl.signin", "wl.skydrive_update" };
- LiveLoginResult authenticationResult = await _authenticationClient.InitializeAsync(scopes);
- if (authenticationResult.Status == LiveConnectSessionStatus.Connected)
- {
- Session = authenticationResult.Session;
- _client = new LiveConnectClient(Session);
- IsFirstConnectionToSkyDrive = await CreateSkyDriveFolderAsync();
- await LoadProfileAsync(Session);
- _userLogged = true;
- }
- }
La création du repertoire “Urza” sur SkyDrive est assez simple, il suffira de poster les informations nécessaire à sa création. puis de sauvegarder son numéro d’identification dans les paramètres itinérant. Ce numéro d’identification qui est sous la forme “folder.9607f0bf305f86d2.9607F0BF305F86D2!681” nous servira comme chemin pour l’upload du fichier de la collection des cartes. A noter que je retourne true ou false, ce qui permet de determiner dans la propriété IsFirstConnectionToSkyDrive si c’est la 1ere fois que le répertoire a été crée.
Code Snippet
- public static async Task<Boolean> CreateSkyDriveFolderAsync()
- {
- if (Session == null) return false;
- try
- {
- IDictionary<String, Object> body = new Dictionary<String, Object>();
- body["name"] = "Urza";
- body["description"] = "UrzaGatherer repository folder";
- LiveOperationResult result = await _client.PostAsync("me/skydrive", body);
- IDictionary<String, Object> r = result.Result;
- URZASettings.SaveRoamingSkyDriveFolder(r["id"].ToString());
- return true;
- }
- catch (LiveConnectException lcex)
- {
- if (lcex.ErrorCode=="resource_already_exists")
- return false;
- throw;
- }
- }
L’upload du fichier sur SkyDrive ce fait à chaque fois que l’utilisateur ajoute ou supprime une carte dans sa collection, est également assez simple,
il suffit d’utiliser la méthode BackGroundUploadAsync du SDK Live.
Cette méthode prend comme paramère :
- Le FolderId (Ex. folder.9607f0bf305f86d2.9607F0BF305F86D2!681),
- Le nom du fichier (Attention, le nom de fichier doit avoir IMPERATIVEMENT l’extension .txt),
- Le contenu fichier à sauvegarder,
- Puis la valeur true pour indiquer que l’on veut ecraser l’ancien fichier.
Ensuite, nous sauvegardons la source du fichier dans les paramètres itinérant, qui correspond à une Uri du type :
https://storage.live.com/s1pBoQfS-WhonrKV7bCPsqQdDOLTtuQShJ76mz4EwW9_2OsgyP5M4_aY7BncUOTpfWsdsrHKID31j4F9Huo7h31W9DhUbl9WjFIa0hpeP7WDCJs3W4CEFMfOZyXnofcbCgn/UserData.txt:Binary,Default/UserData.txt
Et ceci car nous la réutiliseront par la suite.
Code Snippet
- public static async Task<Boolean> UploadFileAsync(String fileName)
- {
- if (Session==null) return false;
- try
- {
- var file = await ApplicationData.Current.LocalFolder.GetFileAsync(fileName);
- var folderId = URZASettings.GetRoamingSkydriveFolder();
- if (folderId == String.Empty)
- {
- //Hum this mean that we have a problem
- return false;
- }
- LiveOperationResult liveOpResult =
- await _client.BackgroundUploadAsync(folderId, fileName, file, true);
- URZASettings.SaveRoamingSkyDriveFileFolder(liveOpResult.Result["source"].ToString());
- return true;
- }
- catch (Exception ex)
- {
- return false;
- }
- }
Ensuite tout se passe au chargement de la vue Expansion, ou j’appelle la méthode DeserializeUserDataAsync().
Cette méthode à pour but de déserialiser le flux XML pour en construire une classe URZAUserData, afin de mettre la propriété IsCheck à true, en fonction de la présence
de la carte dans le fichier.
Code Snippet
- private static URZAUserData DeserializeUserData(Stream stream)
- {
- XmlSerializer x = new XmlSerializer(typeof(URZAUserData));
- URZAUserData data = (URZAUserData)x.Deserialize(stream);
- return data;
- }
- public async Task DeserializeUserDataAsync()
- {
- URZAUserData data = await internalDeserializeUserDataAsync();
- if (data == null) return; //Nothing to deserialize
- var query = from expansion in data.Expansions where expansion.ExpansionId == CurrentExpansion.id select expansion;
- if (query.ToList<URZAUserDataExpansion>().Count == 0) return;
- var UserExpansion = query.First<URZAUserDataExpansion>();
- var UserCards = UserExpansion.UserCards;
- var cards = CurrentExpansion.cards;
- foreach (var userCard in UserCards)
- {
- var queryLookup = from card in cards where card.id == userCard.Id select card;
- queryLookup.First<URZACard>().IsChecked = true;
- }
- }
Néanmoins, avant on va la 1ere fois tester si la version locale du fichier est plus récente que la version sur SkyDrive et en fonction récuperer le bon flux de données
Code Snippet
- public async Task<URZAUserData> internalDeserializeUserDataAsync()
- {
- var folder = ApplicationData.Current.LocalFolder;
- var folderId = URZASettings.GetRoamingSkydriveFolder();
- //First, test if it's the first fresh install or if whe don't have the SkyDrive folderID roaming state
- //does this means that the file does not exist also on skydrive
- if (SkyDriveHelper.IsFirstConnectionToSkyDrive || folderId==String.Empty)
- {
- if (!await folder.IsFileExistsAsync(USERDATA_FILE))
- return null;
- try
- {
- var stream = await folder.OpenStreamForReadAsync(USERDATA_FILE);
- return DeserializeUserData(stream);
- }
- catch (Exception)
- {
- //for any reason return null so the file will be re-created
- return null;
- }
- }
- //Else If isn't the first connection
- //First test if the file exist localy if not download it from skydrive
- if (!await folder.IsFileExistsAsync(USERDATA_FILE))
- {
- if (!NetworkInterface.GetIsNetworkAvailable() || !SkyDriveHelper.IsUserLogged)
- {
- //the locale file have to be created localy
- return null;
- }
- try
- {
- var storageFile = await folder.CreateFileAsync(USERDATA_FILE, CreationCollisionOption.ReplaceExisting);
- var fileId = URZASettings.GetRoamingSkydriveFile();
- await SkyDriveHelper.DownloadFileAsync(fileId, storageFile);
- var stream = await folder.OpenStreamForReadAsync(USERDATA_FILE);
- return DeserializeUserData(stream);
- }
- catch (Exception ex)
- {
- //for any reason recreate the file
- return null;
- }
- }
- //the file exist localy
- var localStream = await folder.OpenStreamForReadAsync(USERDATA_FILE);
- var localData = DeserializeUserData(localStream);
- if (!NetworkInterface.GetIsNetworkAvailable() || !SkyDriveHelper.IsUserLogged)
- {
- return localData;
- }
- try
- {
- //No need to check Again
- if (SkyDriveHelper.IsRemoteFileVersionAlreadyChecked) return localData;
- var source = URZASettings.GetRoamingSkydriveFile();
- if (source == String.Empty)
- {
- return localData;
- }
- var roamingStream = await SkyDriveHelper.DownloadFileAsync(source);
- var roamingData = DeserializeUserData(roamingStream);
- SkyDriveHelper.IsRemoteFileVersionAlreadyChecked = true;
- if (roamingData.Version > localData.Version)
- {
- internalSerializeUserDataAsync(roamingData);
- return roamingData;
- }
- else
- {
- UploadOnSkyDriveAsync();
- return localData;
- }
- }
- catch (Exception ex)
- {
- //For any reason re-create the local file
- return null;
- }
- }
Le téléchargement du fichier est assez simple, car il suffit d’utiliser la source (correspondant à l’uri vue plus haut) avec la méthode BackgroundDownloadAsync().
Code Snippet
- public static async Task<Stream> DownloadFileAsync(String source)
- {
- Stream stream = null;
- if (Session == null) return null;
- try
- {
- var LiveOperationResult = await _client.BackgroundDownloadAsync(source);
- stream=LiveOperationResult.Stream.AsStreamForRead();
- }
- catch (System.Exception ex) //file not found
- {
- stream = null;
- }
- return stream;
- }
Ainsi avec une API assez réduite (à peine 5 fonctions), vous allez pouvoir vous connecter sur Skydrive et y créer vos fichiers et vos répertoires.
A bientôt
Eric Vernié