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:
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 :
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
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.
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.
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 :
Visual va ajouter automatiquement :
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); }
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);
}
Néanmoins, il faut prendre en compte désormais que l’application peut être chargée de deux manières différentes.
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.
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); } }
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>
<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>
<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>
<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(); }
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); } }
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();
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>(); }
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;
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 :
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(); }); }
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();
Task.Run(async () =>
await this.MapCardsAsync(expansion);
CardsAlreadyLoaded = true;
RaiseAllCardsLoadedAsync(CoreDispatcherPriority.High);
});
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); } }.
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);
}.
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) :
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; }
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é.
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); }
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);
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.
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 :
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.
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
protected override void OnFileOpenPickerActivated(Windows.ApplicationModel.Activation.FileOpenPickerActivatedEventArgs args) { var fileOpenPickerPage = new UrzaGatherer.Views.FileOpenPickerPage(); fileOpenPickerPage.Activate(args); }
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.
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(); }
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.
_vueData.FilePickerArgs = args;
NavigateToExtendedSplachScreen(args.SplashScreen);
private void NavigateToExtendedSplachScreen(SplashScreen splachscreen)
ExtendedSplachScreen exSplash = new ExtendedSplachScreen(splachscreen, _vueData);
Window.Current.Content = exSplash;
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); } }
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);
//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); }
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.
Windows 8 Metro se base maintenant sur des vignettes pour lancer les applications. Une vignette est une super icone (http://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 :
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é http://msdn.microsoft.com/en-us/library/windows/apps/hh761491.aspx
Dans notre exemple, nous choisirons le modèle TileWideSmallImageAndText02,
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);
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());
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;
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.
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>.
<Page.BottomAppBar>
<AppBar x:Name="PageAppBar" Padding="10,0,10,0">
<Grid>
<Grid.ColumnDefinitions>
<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" />
</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); }
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,
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é }
protected override async void OnLaunched(LaunchActivatedEventArgs args)
if (args.PreviousExecutionState == ApplicationExecutionState.Running)
_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();
var rootFrame = new Frame();
if (_vueData.SecondaryTileArgs.Length > 0)
_vueData.FindExpansion();
rootFrame.Navigate(typeof(Expansions), vueData);
rootFrame.Navigate(typeof(Home), vueData);
Window.Current.Content = rootFrame;
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(); }
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; }
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" />
<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);
SecondaryTile secondaryTyle = new SecondaryTile(expansionid);
bool IsUnPinned = await secondaryTyle.RequestDeleteForSelectionAsync(rect, Windows.UI.Popups.Placement.Below);
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).