Comment “cuisiner” une application Windows 8 avec XAML et C# en une semaine–Jour 3

 

Nous allons aujourd’hui nous pencher sur l’intégration de notre application au sein de Windows 8 Metro.

Nous avions déjà intégré le mode snapped dans les précédents épisodes et nous allons dans cet
article nous attaquer aux sujets suivants:

  • Contrat de recherche
  • Contrat de partage
  • Contrat d’ouverture de fichier
  • Vignette interactive
  • Vignettes secondaires

Pour réussir une application Windows 8 Metro, il est important de bien intégrer ces cinq fonctionnalités, par contre elles ne sont pas obligatoires.

Pour les retardataires, vous pouvez retrouver les jours précédant ici.

Jour 0    (la Consumer Preview)

Jour 1 :  (Consumer preview)

Jour 2 :  (Release Preview)

 Jour 2 Optimisé :(Release preview)

 

Téléchargement de la release Preview et des outils.

 

Comme d'habitude le code se trouve ici :

 

Les contrats :

Un contrat constitue la définition d’une interface technique entre une application et Windows 8 Metro. C’est un sujet important puisqu’il permet à votre application d’avoir de nouveaux points d’entrées en plus de sa vignette principale.

Il va vous permettre de vous intégrer au sein de services primordiaux de Windows 8 comme la recherche, le partage de données ou bien encore la sélection de fichiers.

L’utilisateur peut invoquer les deux principaux contrats via la barre des charms

Barre Charms
 
Ce n’est donc pas un sujet à prendre à la légère lors de votre réflexion car cela peut fortement manquer à vos utilisateurs si vous omettez
des contrats que vous auriez pu couvrir.

Contrat de recherche :

Ce contrat va permettre à l'utilisateur de faire une recherche dans votre application au même niveau qu'il ferait une recherche dans
ses fichiers, ses applications ou n'importe quelles autres applications qui implémentent le contrat de recherche.

ContratSearch

 

Pour implémenter ce contrat, Visual Studio va vous aider en grande partie, en y incluant une page de recherche ainsi que la gestion du contrat de recherche
dans l'application.

Il faudra pour ce faire ajouter un nouvel élément de type Search Contract. Comme indiqué sur la figure suivante :

 AddContratSearch

Visual va ajouter automatiquement :

  • Dans le manifeste la gestion du contrat de recherche
    SearchManifest

  • Insérer dans le fichier App.xaml.cs, la méthode surchargée OnSearchActivated qui comme son nom
    l’indique va nous permettre de réagir lorsque le contrat de recherche est invoqué.

     protected override void OnSearchActivated(Windows.ApplicationModel.Activation.SearchActivatedEventArgs args)
    
         {
    
             //Stop any current task 
    
              _vueData.CancelAsync(true);
    
             _vueData.QueryText = args.QueryText.ToLower();            
    
             UrzaGatherer.Views.SearchResultsPage.Activate(_vueData, args.PreviousExecutionState);
    
         }
    
  • La page de recherche nommée ici SearchResultsPage.xaml, qui fournit toute la structure de recherche et d’affichage. Bien évidement vous pouvez avoir

    votre propre page de recherche.

 

Néanmoins, il faut prendre en compte désormais que l’application peut être chargée de deux manières différentes.

  • Soit normalement via la page de démarrage, et l’application utilise son flow de chargement habituel avec le

    déclenchement de l’évènement OnLaunched.

  • Soit via l’invocation du contrat de recherche qui déclenche l’évènement OnSearchActivated, mais sans passer par

    OnLaunched, ce qui  a une incidence sur notre mécanique d’initialisation des données et de l’application en elle-même.

    Du coup, Le contrat de recherche va avoir plusieurs incidences sur notre code. Tout d’abord, comme nous ne passons pas forcement

    par le flux normal d’activation de l’application, il va falloir prévoir l’initialisation des données au chargement de la page de recherche.

    Mais comme le contrat de recherche peut être invoqué alors que l’application est déjà chargée, il va falloir prévoir, des mécanismes de protection des appels

    asynchrones, pour éviter d’éventuels conflits.

    Remarque : Dans le scénario ou l’application n’est chargée que lors de l’activation du contrat de recherche, comment déboguer cette situation ? c’est simple et

    illustré sur la figure suivante :

    DebugSearch 

    Dans les propriétés du projet il suffit de cocher cette case, appuyez sur la touche F5, l’application se met alors en

    attente. Il suffit alors de mettre n point d’arrêt sur la méthode OnSearchActivated et c’est tout !!.

La page de recherche fournit toute la structure pour afficher le résultat de la recherche,

comme illustré sur la figure suivante, c’est sur ce modèle que je me suis appuyé pour ne pas réinventer la roue.

Resultat Recherche

Comme toutes les autres vues, la page de recherche, dérive de la classe LayoutAwarePage, et pour communiquer le résultat de la recherche, il

faudra remplir des listes à la fois pour le filtre ainsi que pour le résultat qui seront affectés au DefaultViewModel.

 private void Search()
        {
            _vueData.GetSuggestionList();
            _vueData.Search();
            this.DefaultViewModel["Filters"] = _vueData.SearchFilter;
            this.DefaultViewModel["ShowFilters"] = _vueData.SearchFilter.Count > 1;
            DetachHandler();
        } 
void  Filter_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
           
            progressRing.IsActive = false;
            // Determine what filter was selected
            var selectedFilter = e.AddedItems.FirstOrDefault() as SearchFilter;
            if (selectedFilter != null)
            {
                // Mirror the results into the corresponding Filter object to allow the
                // RadioButton representation used when not snapped to reflect the change
                selectedFilter.Active = true;                                              
                this.DefaultViewModel["Results"] =_vueData.SearchResult[selectedFilter.Name];
               // Ensure results are found
                object results;
                ICollection resultsCollection;
                if (this.DefaultViewModel.TryGetValue("Results", out results) && 
                    (resultsCollection = results as ICollection) != null && resultsCollection.Count != 0)
                {
                    _currentCards = (IEnumerable<URZACard>)results;
                   _vueData.MapPictureCardsAsync(_currentCards);                   
                    VisualStateManager.GoToState(this, "ResultsFound", true);          
                    return;
                }
            }
            // Display informational text when there are no search results.
            VisualStateManager.GoToState(this, "NoResultsFound", true);           
        }
} 

Dans la page XAML, le lien des données se fait au travers d’objets CollectionViewSource

 <Page.Resources>
        <CollectionViewSource x:Name="resultsViewSource" Source="{Binding Results}"/>
        <CollectionViewSource x:Name="filtersViewSource" Source="{Binding Filters}"/>
        <common:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>  
</Page.Resources>

Pour les filtres le modèle utilise un contrôle ItemsControl pour afficher une collection de RadioButton

      <ItemsControl                   
                    x:Name="filtersItemsControl"
                    ItemsSource="{Binding Source={StaticResource filtersViewSource}}"
                    Visibility="{Binding ShowFilters, Converter={StaticResource BooleanToVisibilityConverter}}"
                    Margin="120,-3,120,30">
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <StackPanel Orientation="Horizontal"/>
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <RadioButton                                
                                Content="{Binding Description}"
                                GroupName="Filters"
                                IsChecked="{Binding Active, Mode=TwoWay}"
                                Checked="Filter_Checked"
                                Style="{StaticResource TextRadioButtonStyle}"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
     </ItemsControl>
 

Pour le résultat de la recherche, c’est une GridView Traditionnelle

   <GridView
                    x:Name="resultsGridView"
                    AutomationProperties.AutomationId="ResultsGridView"
                    AutomationProperties.Name="Search Results"
                    TabIndex="1"
                    Grid.Row="1"
                    Margin="0,2,0,0"
                    Padding="110,0,110,46"
                    SelectionMode="None"
                    IsItemClickEnabled="True"
                    ItemClick="resultsGridView_ItemClick"
                    ItemsSource="{Binding Source={StaticResource resultsViewSource}}"
                    ItemTemplate="{StaticResource UrzaSearchGridView}">                                        
 </GridView>

Le chargement de la page de recherche, se fait à l’aide de la méthode statique Activate, générée par Visual Studio. Méthode dont j’ai modifié la

signature, pour prendre comme paramètre la classe VueData, car c’est elle qui transporte tout l’état de mes données à un instant T, et que je vais modifier pour effectuer la recherche.

 public static void Activate(VueData vuedata, ApplicationExecutionState 
previousExecutionState)
        {           
            var previousContent = Window.Current.Content;           
            var frame = previousContent as Frame;
            SearchResultsPage page = new SearchResultsPage();
            page._previousContent = previousContent;
            page._previousExecutionState = previousExecutionState;
            page.LoadState(vuedata, null);
            Window.Current.Content = page;          
            Window.Current.Activate();
        }

Au chargement j’appelle également la méthode MapBlockAsync() qui a pour rôle de charger les données concernant les Blocks et les Expansions (et

éventuellement de télécharger le fichier JSON), car rappelez-vous, l’application n’est peut-être pas déjà chargée lors de l’invocation de la

recherche.

Cette méthode lève désormais l’évènement BlocksLoaded, pour notifier l’appelant que les données sont disponibles.

 protected  override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
        {
            _vueData = navigationParameter as VueData;
            this.QueryText = _vueData.QueryText;
            // Communicate results through the view model
            this.DefaultViewModel["QueryText"] = '\u201c' + this.QueryText + '\u201d';
            this.DefaultViewModel["CanGoBack"] = this._previousContent != null;
            _vueData.InitInternalSettings();                                                
            _vueData.BlocksLoaded += _vueData_BlocksLoaded; 
            _vueData.MapBlocksAsync();                                                    
        }
public async void MapBlocksAsync()    
        {           
            try
            {
                if (this.BlocksAlreadyLoaded)
                {
                    RaiseBlocksAvailable();
                    return;
                }
                else
                {
                    await _dataSource.LoadDataAsync();
                   
                }
            }
            catch (Exception ex)
            {                           
                        RaiseErrorAsync(ex);                  
            }
        }

Dans la classe VueData, j’y ai ajouté une méthode Search() qui parcourt toutes les données et qui

alimente les listes nécessaires à l’affichage des filtres et du résultat.

 List<SearchFilter> _searchFilter;
public List<SearchFilter> SearchFilter { get { return _searchFilter; } }

Dictionary<String, List<URZACard>> _searchResult;

public Dictionary<String, List<URZACard>> 

SearchResult { get { return _searchResult; } }
public void Search()
        {
            _searchFilter = new List<SearchFilter>();
            _searchFilter.Add(new SearchFilter(UrzaResources.AllSearch, 0, true));
            var all = new List<URZACard>();           
            _searchResult = new Dictionary<String, List<URZACard>>();
            _searchResult.Add(UrzaResources.AllSearch, all);
            var blocks = this.Blocks;                       
            foreach (var block in blocks)
            {
               foreach (var expansion in block.expansions)
               {
                   List<URZACard> items = null;
                   items = Search(expansion);
                   if (items.Count > 0)
                   {
                     _searchResult.Add(expansion.name,items);                   
                      all.AddRange(items);                                            
                     _searchFilter.Add(new SearchFilter(expansion.name, items.Count, false));
                   }
                }
            }                       
            _searchFilter[0].Count = all.Count;
        }
 public List<URZACard> Search(URZAExpansion expansion)
        {           
            IEnumerable<URZACard> query=null;
                if (QueryText == "*")
                {
                    query = from card in expansion.cards select card;
                }
                else
                {
                    query = from card in expansion.cards where card.name.ToLower().Contains(QueryText.ToLower()) || 
                                                               card.text.ToLower().Contains(QueryText.ToLower()) ||                                     
                                                               card.flavor.ToLower().Contains(QueryText.ToLower())
                                                         select card;
                }
            return query.ToList<URZACard>();           
        }

 

Néanmoins, à ce stade deux choses me chiffonnent.

La première, c’est que les performances de la recherche ne sont pas au rendez-vous lorsque l’application est déjà chargée. En effet, les cartes ne

sont créées que lorsqu’une Expansion est sélectionnée.

La seconde c’est que je voudrais créer une liste de suggestions de recherche basée sur les noms des cartes,

comme sur la figure suivante :

Liste suggestions

Or pour cela, il faut que toutes les cartes soient déjà chargées.

J’ai donc ajouté la méthode LoadAllCardAsync  à ma classe VueData qui a pour rôle de charger toutes les cartes en arrière-plan et de lever l’évènement

AllCardsLoaded lorsqu’elles sont disponibles.  Réglant ainsi mes deux soucis.

Néanmoins, la liste des suggestions,  comme c’est une exécution asynchrone, n’apparaitra que lorsque toutes les cartes seront chargées.

J’ai ajouté également un point de synchronisation avec un objet sémaphore (SemaphoreSlim) pour éviter toute

réentrance, car cette méthode est susceptible d’être appelée plusieurs fois de manière quasi simultanée.

 public async void LoadAllCardsAsync()
        {
            //We have to wait because the searchPage can also access to this method in the same time
            await _semaphoreAllCardsLoaded.WaitAsync();
            if (CardsAlreadyLoaded)
            {
                RaiseAllCardsLoadedAsync(CoreDispatcherPriority.Low);                
               _semaphoreAllCardsLoaded.Release();
                return;
            }         
            var blocks = this.Blocks;          
            Task.Run(async () =>
                {
                                         
                        foreach (var block in blocks)
                        {
                            foreach (var expansion in block.expansions)
                            {                               
                                await this.MapCardsAsync(expansion);
                            }
                        }
                                  
                    CardsAlreadyLoaded = true;                                                          
                    RaiseAllCardsLoadedAsync(CoreDispatcherPriority.High);
                    _semaphoreAllCardsLoaded.Release();
                });
        }

Remarque : En règle générale, un code parallèle et ou asynchrone ce porte mieux quand on évite les  point de synchronisation, pour

des raisons évidentes de performances et d’éventuelles erreurs de type Deadlock, mais il y a des cas, ou on ne peut s’en passer. Il est donc

important, avant même d’écrire une seule ligne de code de penser parallèle, et dans le cas d’une application Windows 8 de penser asynchronisme. C’est vrai que

.NET nous aide grandement avec la TPL et les sucres syntaxique async et await, mais ceci n’exclut en aucun cas une réflexion en avance de phase, plutôt qu’en codant un peu à la volée comme je le fait lors de ce portage de l’application JavaScript de David Catuhe.

Par exemple, le code de la méthode LoadCardAsync, n’est pas optimum, car que ce passerai-t-il si par exemple une erreur survenait ? Sans doute un point de synchronisation qui attendrait indéfiniment, car nous ne sommes pas sûr que le release soit fait. Je vous laisse le soin de choisir la meilleure méthode, mais il est

de bon ton de ne pas laisser un point de synchronisation indéfiniment,  et justement la méthode WaitAsync est surchargée et peut prendre un délai d’attente ou un CancellationToken, voir les deux. Il

est donc important, en fonction de votre code de bien choisir son scénario afin de créer un code le plus robuste possible.

Une fois toutes les cartes chargées sur l’évènement AllCardsLoaded, je construis la liste des suggestions.

Pour construire une telle liste, c’est très simple, il suffit de faire appel à

la classe Windows.ApplicationModel.Search.SearchPane, de s’abonner à l’évènement SuggestionsRequested de la vue courante, et le tour est joué

 public static void AddSearchSuggestion(String[] suggestions)
        {
            _suggestions = suggestions;
        SearchPane.GetForCurrentView().SuggestionsRequested+=URZASearchSuggestion_SuggestionsRequested;
        }
 
static void URZASearchSuggestion_SuggestionsRequested(SearchPane sender, SearchPaneSuggestionsRequestedEventArgs args)
        {
            string query = args.QueryText.ToLower();
            string[] _suggestions = { "Abyssal", "Urza", "Cold Snap", "Ice Age", "Vision", "Dark Ascension", "Exodus", "Innistrad" };
            foreach (var term in _suggestions)
            {
                if (term.StartsWith(query))                    
                      args.Request.SearchSuggestionCollection.AppendQuerySuggestion(term);
            } 
        }.

 

Contrat de partage

Le contrat de partage va vous permettre de ne plus avoir à vous préoccuper de coder des services de partage sur les réseaux sociaux. En

effet, auparavant si vous souhaitiez pouvoir publier sur Facebook, sur Twitter ou sur tout autre réseau vous deviez intégrer le code dans votre application.

Avec le contrat de partage, vous pouvez dorénavant indiquer que vous êtes un producteur de données de partage ou un service capable de les

publier (vers un réseau social ou toute application capable de consommer ces données comme par exemple un courrier) :

Partage

Pour invoquer le service de partage, c’est très simple aussi, il suffit d’utiliser la classe Windows.ApplicationModel.DataTransfer.DataTransferManager,

de s’abonner à l’évènement DataRequested de la vue courante

    public static void AddShareData()
        {
            DataTransferManager.GetForCurrentView().DataRequested += URZAShareData_DataRequested;
        }

L’évènement DataRequest, passe comme argument DataRequestedEventArgs qui va nous permettre de déterminer le

type de données que nous souhaitons partager. Il est possible de partager des images, du texte, des fichiers, mais également directement de l’html. 

Dans notre exemple, nous partageons, du texte request.data.SetText(), mais également des images, request.Data.SetBitmap() ,

ainsi que de l’HTML. Ce dernier nous permettant de formater à notre convenance ce que nous voulons envoyer. C’est

ensuite à l’application cible, de choisir ce qu’elle souhaite afficher. Pour ce dernier format, il est impératif, d’appeler la méthode Helper Windows.ApplicationModel.DataTransfer.HtmlFormatHelper.CreateHtmlFormat() pour que l’HTML soit bien formaté.

Partage2

  static  void URZAShareData_DataRequested(DataTransferManager sender, DataRequestedEventArgs args)
        {
            var request = args.Request;
            var deferral = request.GetDeferral();    
            Request.Data.Properties.ApplicationName = "UrzaGatherer";
            request.Data.Properties.Title = Card.name;
            request.Data.Properties.Description = Card.ExpansionName;
            if (Card.Picture == null)
            {
                request.FailWithDisplayText(UrzaResources.SharingPicture);
                return;   
            }                                       
                String uriPath;                         
                uriPath = Card.PictureInfo.RemotePath
                RandomAccessStreamReference imageStreamRef = RandomAccessStreamReference.CreateFromUri(new Uri(uriPath));                
                request.Data.SetHtmlFormat(FormatHtml());                               
                request.Data.Properties.Thumbnail = imageStreamRef;
                request.Data.SetBitmap(imageStreamRef);
                request.Data.SetText(Card.text);              
                deferral.Complete();                                      
        }
 static String FormatHtml()
        {           
            String  html =String.Format(@"<p> {0} </br>  <img src='{1}'>.</p>",Card.text,Card.PictureInfo.RemotePath);
            return Windows.ApplicationModel.DataTransfer.HtmlFormatHelper.CreateHtmlFormat(html);
        }

Remarque : La liste des applications cibles est affichée par Windows 8 en fonction de ce

que vous partagez. Si vous ne partagez pas d’images par exemple, les applications ne souhaitant recevoir que des images, ne s’afficheront pas.

 

Contrat d’ouverture de fichier

Le contrat d’ouverture de fichier va permettre à votre application d’être fournisseur de fichiers (comme par exemple une application Skydrive qui

fournirait à d’autres applications le contenu de votre Skydrive comme si il était local).

Pour UrzaGatherer, nous allons fournir à toutes les applications voulant requêter une image la possibilité de récupérer une image

de notre collection.

Ainsi prenons l’exemple d’un nouveau courrier à écrire :

courrier

En cliquant sur Pièces Jointes, je peux choisir de rajouter une pièce jointe et l’écran de sélection de fichiers de Windows 8 Metro s’ouvre, en sélectionnant Fichier, je

peux alors choisir dans une liste d’application, celles qui me fourniront des images.

 

courrier2

Pour implémenter ce contrat FileOpenPicker, comme d’habitude, Visual Studio va nous aider.

Il suffira d’ajouter un nouvel élément de type FileOpenPicker

FileOpenPicker

Visual va ajouter automatiquement :

  • Dans le manifeste la gestion du contrat. Ici nous choisissons le type de fichier .jpg

    filepicker2 

  • Insérer dans le fichier App.xaml.cs, la méthode surchargée OnFileOpenPickerActivated

    qui comme son nom l’indique va nous permettre de réagir lorsque le contrat est invoqué.

     protected override void OnFileOpenPickerActivated(Windows.ApplicationModel.Activation.FileOpenPickerActivatedEventArgs args)
    
            {
    
                var fileOpenPickerPage = new UrzaGatherer.Views.FileOpenPickerPage();
    
                fileOpenPickerPage.Activate(args);
    
            } 
    

    C’est donc une troisième manière de charger l’application, mais nous réutiliserons le même flux de chargement, comme si l’application était chargée

    via la page de démarrage.

  • La page qui permettra d’afficher les images, mais nous ne l’utiliserons pas. Car nous utiliserons directement la vue Extension.

    Au lieu d’ouvrir la page de détail d’une carte, elle ajoutera dans une liste de fichiers, les fichiers à ouvrir par l’application appelante.

Pour être fournisseur de fichier, il faut donc remplir une liste des fichiers à l’application appelante. J’ai donc rajouté la propriété FilePickerArgs dans ma classe VueData, qui

me permettra de savoir si l’application est appelée en tant que fournisseur de fichiers images.

    private FileOpenPickerActivatedEventArgs _fileOpenPickerActivatedEventArgs;                       
                public FileOpenPickerActivatedEventArgs FilePickerArgs {
                    get { return _fileOpenPickerActivatedEventArgs;}
                    set {
                         _fileOpenPickerActivatedEventArgs = value;                    
                         _fileOpenPickerActivatedEventArgs.FileOpenPickerUI.FileRemoved += FileOpenPickerUI_FileRemoved;                       
                ;}
            }
                void FileOpenPickerUI_FileRemoved(FileOpenPickerUI sender, FileRemovedEventArgs args)
                {
                    //TODO Remove de la liste       
                }
 
Sur l’évènement OnFileOpenPickerActivated, qui posséde l’évènement FileOpenPickerActivatedEventArgs, 

je le passe à ma classe VueData. 
  protected override void OnFileOpenPickerActivated(Windows.ApplicationModel.Activation.FileOpenPickerActivatedEventArgs args)
        {                       
            _vueData.FilePickerArgs = args;            
            NavigateToExtendedSplachScreen(args.SplashScreen);                       
        }
private void NavigateToExtendedSplachScreen(SplashScreen splachscreen)
        {
            ExtendedSplachScreen exSplash = new ExtendedSplachScreen(splachscreen, _vueData);  
            Window.Current.Content = exSplash;
            Window.Current.Activate();
        }

 

A noter ici que je reprends le flow normal d’exécution en invoquant le splachScreen étendu.

Il suffit ensuite de tester dans la vue Expansion sur l’évènement cardsItemsGridView_ItemClick

lorsqu’une carte est sélectionnée, si nous somme en mode normal ou en mode FileOpenPicker.

 private async void cardsItemsGridView_ItemClick(object sender, ItemClickEventArgs e)
        {
            DetachHandlers();
            _vueData.CurrentCard = (URZACard)e.ClickedItem;
            if (_vueData.FilePickerArgs == null)
            {               
                this.Frame.Navigate(typeof(Cards), _vueData);
            }
            else
            {
                //Get the picture for the FilePicker Contract                       
                await _vueData.OpenPictureAsync(_vueData.CurrentCard);                               
            }                                
        }

La méthode OpenPictureAsync, ne fait que tester si le fichier est disponible, sinon il le télécharge et

l’ajoute dans la liste des fichiers à retourner à l’application appelante.

 public async Task OpenPictureAsync(URZACard card)
        {
            var storageFile = await Helper.OpenFileAsync(card.PictureInfo.LocalFolder, card.PictureInfo.FileName);
            if (storageFile==null)
            {           
                if (NetworkInterface.GetIsNetworkAvailable())
                {
                    await Helper.DownloadPicture2Async(TokenSource.Token, card.PictureInfo);
                    storageFile = await Helper.OpenFileAsync(card.PictureInfo.LocalFolder, card.PictureInfo.FileName);
                }
            }
            String id=card.PictureInfo.FileName;
            this._fileOpenPickerActivatedEventArgs.FileOpenPickerUI.AddFile(id, storageFile);           
        }

 

Et c’est tout.

En fin lorsque l’application appelante redemande l’ouverture d’un fichier, FileOpenPicker utilise

par défaut UrzaGatherer, mais il est possible de choisir dans sa liste un autre fournisseur.

filepicker3

 

Vignette Interactive

Windows 8 Metro se base maintenant sur des vignettes pour lancer les applications. Une vignette est une super icone (https://msdn.microsoft.com/en-us/library/windows/apps/hh779724.aspx)

qui peut être dynamique et mise à jour par l’application, par une tâche de fond ou par un service de notification :

 

 tile

Envoyer une mise à jour de vignette

Nous allons dans le cadre de UrzaGatherer mettre à jour cette tuile à chaque fois qu’une carte est ouverte.

Tout d’abord, il faut choisir le type de modèle que l’on souhaite utiliser voir les différents types proposé https://msdn.microsoft.com/en-us/library/windows/apps/hh761491.aspx

Dans notre exemple, nous choisirons le modèle TileWideSmallImageAndText02,

IC563444

Qui est défini en XML

<tile>

<visual>

    <binding template="TileWideSmallImageAndText02">

      <image id="1" src="image1.png" alt="alt text"/>

      <text id="1">Text Header Field 1</text>

      <text id="2">Text Field 2</text>

      <text id="3">Text Field 3</text>

      <text id="4">Text Field 4</text>

      <text id="5">Text Field 5</text>

    </binding>

  </visual>

</tile>

 

Ce qu’il faut savoir, c’est que ce modèle est au format XML que nous allons renseigner, à l’aide d’API de manipulation XML. Tout d’abord

nous utilisons la Classe TileUpdateManager, pour instancier le modèle que l’on souhaite manipuler. Et Ensuite on utilise les APIs courantes de manipulation XML.

 var tileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileWideSmallImageAndText02);
var tileTextAttributes = tileXml.GetElementsByTagName("text");

tileTextAttributes[0].AppendChild(tileXml.CreateTextNode("UrzaGatherer"));

tileTextAttributes[1].AppendChild(tileXml.CreateTextNode(card.name));

tileTextAttributes[2].AppendChild(tileXml.CreateTextNode(card.Expansion.name));

tileTextAttributes[3].AppendChild(tileXml.CreateTextNode(card.Expansion.block.name));

var tileImageAttributes = tileXml.GetElementsByTagName("image");

var xnlNode = tileImageAttributes[0].Attributes.GetNamedItem("src");

String src = card.PictureInfo.RemotePath;

xnlNode.InnerText = src;

var squareTileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileSquareImage);
var squareTileImageAttributes = squareTileXml.GetElementsByTagName("image");

var x = squareTileImageAttributes[0].Attributes.GetNamedItem("src");

x.InnerText = card.PictureInfo.RemotePath; ;
var node = tileXml.ImportNode(squareTileXml.GetElementsByTagName("binding").Item(0), true);

tileXml.GetElementsByTagName("visual").Item(0).AppendChild(node);

var tileNotification = new TileNotification(tileXml);
tileNotification.Tag = card.id.ToString();
var tileUpdater = Windows.UI.Notifications.TileUpdateManager.CreateTileUpdaterForApplication();
tileUpdater.EnableNotificationQueue(true);
tileUpdater.Update(tileNotification);

 

Néanmoins, c’est relativement fastidieux, et je trouve loin de la philosophie « .NET », c’est pourquoi, j’ai utilisé une fois n’est pas coutume du

code qui est fourni dans les exemples de Windows 8 (App tiles and badges sample) qui encapsule sous forme de classe

toute la structure de création d’une Vignette. Jugez plutôt.

 

 ITileWideSmallImageAndText02 tileContent = TileContentFactory.CreateTileWideSmallImageAndText02();
tileContent.TextBody1.Text = card.Expansion.block.name;
tileContent.TextBody2.Text = card.Expansion.name;
tileContent.TextBody3.Text = card.name;
 
tileContent.Image.Src = card.PictureInfo.RemotePath;
tileContent.Image.Alt = "Web Image";
ITileSquareImage squareContent = TileContentFactory.CreateTileSquareImage();
squareContent.Image.Src = card.PictureInfo.RemotePath;
squareContent.Image.Alt = "Web image";
tileContent.SquareContent = squareContent;
var tileUpdater = Windows.UI.Notifications.TileUpdateManager.CreateTileUpdaterForApplication();
tileUpdater.EnableNotificationQueue(true);
tileUpdater.Update(tileContent.CreateNotification());

Vous retrouverez tout le code de création et d’utilisation des modéle de création de vignette interactive dans le projet NotificationsExtensions.

Vignettes Secondaires

 

Les vignettes secondaires fonctionnent comme les vignettes principales à ceci près qu’elles doivent donner accès à un autre endroit de

l’application que la page principale.

Dans le cadre de UrzaGatherer, la page des extensions pourra

proposer via son appbar (la barre qui apparait en bas quand on clique avec le

bouton droit de la souris ou quand on glisse son doigt du bord bas de l’écran

vers le centre) de créer une vignette secondaire pour arriver directement sur

l’extension courante :

On va donc commencer par ajouter un contrôle AppBar à notre vue Extension.

 <Page.BottomAppBar>
        <AppBar x:Name="PageAppBar" Padding="10,0,10,0">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="50*"/>
                    <ColumnDefinition Width="50*"/>
                </Grid.ColumnDefinitions>
                <StackPanel x:Name="LeftCommands" Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
                </StackPanel>
                <StackPanel x:Name="RightCommands" Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Right">
                    <Button x:Name="Pin"  Visibility="{Binding PinUnpinSecondaryTile, Converter={StaticResource BooleanToCollapseConverter}}" HorizontalAlignment="Right" Style="{StaticResource PinAppBarButtonStyle}" Click="Pin_Click"  />
                    <Button x:Name="UnPin"  Visibility="{Binding PinUnpinSecondaryTile,Converter={StaticResource 
BooleanToVisibilityConverter}}" HorizontalAlignment="Right" Style="{StaticResource UnpinAppBarButtonStyle}" Click="UnPin_Click"  />
                </StackPanel>
            </Grid>
        </AppBar>
    </Page.BottomAppBar>.

Ici j’ai deux Boutons, Pin et UnPin qui seront visible ou pas en fonction de l’existence d’une

Vignette Secondaire.

Dans le cas où la Vignette n’existe pas on déclenche le code suivant : 

  

 public static async void PinSecondaryTile(FrameworkElement element,URZAExpansion expansion)
        {
                Uri logo = new Uri("ms-appx:///Assets/Logo.png");
                String tileActivationArguments =expansion.block.id +"," + expansion.id.ToString();
                SecondaryTile secondaryTile = new SecondaryTile(expansion.id.ToString(),
                                                                expansion.name,
                                                                expansion.name,
                                                                tileActivationArguments,
                                                                TileOptions.ShowNameOnLogo,
                                                                logo);
                var rect = Helper.GetElementRect(element);
               bool IsPinned=await secondaryTile.RequestCreateForSelectionAsync(rect, Windows.UI.Popups.Placement.Below);
                
        }

Pour créer la vignette secondaire, on utilisera le numéro d’identification de l’expansion, son nom et comme argument (tileActivationArguments),

le numéro d’identification du block, ainsi que celui de l’expansion. Ce dernier argument sera passé à l’application lorsque l’utilisera cliquera sur la

vignette secondaire il faudra le prendre en considération lors du démarrage de l’application.

Du coup, j’ai rajouté dans la classe VueData, la propriété SecondaryTileArgs qui me servira à charger directement la vue Expansion si la

propriété est renseignée.

Du coup au chargement de l’application sur l’évènement OnLaunched, je sauvegarde les arguments

passés à l’application et qui sera transmis automatiquement à la vue ExtendedSpaschScreen.

 

 protected override async void OnLaunched(LaunchActivatedEventArgs args)
        {
         
            if (args.PreviousExecutionState == ApplicationExecutionState.Running)
            {
                Window.Current.Activate();
                return;
            }
            _vueData.SecondaryTileArgs = args.Arguments;
//Code omis pour plus de clarté
}

Dans la vue ExtendedSplachScreen, je test si la propriété est renseignée, si oui, je charge la vue Expansions directement sans passer par la vue Home   

  var rootFrame = new Frame();
            if (_vueData.SecondaryTileArgs.Length > 0)
            {
                _vueData.FindExpansion();
                rootFrame.Navigate(typeof(Expansions), vueData);
            }
            else
            {
                rootFrame.Navigate(typeof(Home), vueData);
            }
           
            Window.Current.Content = rootFrame;
            Window.Current.Activate();  
 

 

Avant de charger la vue, méthode FindExpansion() recherchera la bonne Expansion, en utilisant les arguments passés via la

propriété SecondaryTileArgs .

 public void FindExpansion()
{
 String[] args = this.SecondaryTileArgs.Split(',');
 int blockId=Convert.ToInt32(args[0]);
 int expansionId=Convert.ToInt32(args[1]);
 var queryExpansions = from block in this.Blocks where block.id == blockId select block.expansions;
 var expansions = queryExpansions.First();           
 var queryExpansion = from expansion in expansions where expansion.id == expansionId select expansion;
 this.CurrentExpansion = queryExpansion.First();
}

 

Comme nous l’avons plus haut, dans l‘AppBar, j’ai défini les deux boutons Pin/UnPin qui s’affichent en fonction de l’existence d’une vignette

secondaire.

 Bool ifExist=Windows.UI.StartScreen.SecondaryTile.Exists(id);

 

Pour ce faire sur l’évènement Opened de l’AppBar je renseigne le modèle de vue PinUnpinSecondaryTile que j’ai crée

 void PageAppBar_Opened(object sender, object e)
{  
  String expansionId = 
 _vueData.CurrentExpansion.id.ToString();   
 Boolean isTileExist = URZATileManager.IsSecondaryTileExists(expansionId);
 this.DefaultViewModel["PinUnpinSecondaryTile"] = isTileExist;
}

Dans le fichier XAML, j’ai lié la propriété Visibility des deux boutons à ce modèle et qui passe par deux Converters

BooleanToVisibilityConverter et BooleanToCollapseConverter qui comme leur nom l’indique affiche ou n’affiche pas le contrôle auquel ils sont liés

 

 

 <Button x:Name="Pin"  Visibility="{Binding PinUnpinSecondaryTile, 
Converter={StaticResource BooleanToCollapseConverter}}" 

HorizontalAlignment="Right" Style="{StaticResource PinAppBarButtonStyle}" Click="Pin_Click"  />
<Button x:Name="UnPin"  Visibility="{Binding PinUnpinSecondaryTile,C

onverter={StaticResource BooleanToVisibilityConverter}}" 

HorizontalAlignment="Right" Style="{StaticResource UnpinAppBarButtonStyle}" Click="UnPin_Click"  />

Pour supprimer une vignette secondaire, c’est très simple, on utilise également la classe SecondaryTile

 SecondaryTile secondaryTyle = new SecondaryTile(expansionid);
var rect = Helper.GetElementRect(element);

bool IsUnPinned = await secondaryTyle.RequestDeleteForSelectionAsync(rect, Windows.UI.Popups.Placement.Below);

 

A suivre

Lors de notre prochaine étape, nous intègrerons le support de Live SDK et de Skydrive afin de gérer

votre collection (à savoir indiquer quelles cartes vous possédez et quelles cartes il vous manque).