Part 28 - Working with Animations in XAML

Download the source code for this lesson at http://absolutebeginner.codeplex.com/
In this final lesson, we’ll build the MapNotes app. It is a GPS-aware note app that records not just the note you take but also where you took that note and displays it on a map. Then you’ll be able to read that note and see a map of where you took the note. You can then delete the note or continue on from there.
In addition to working with Phone storage, the Map control and Geolocation, I’ll also add some nice little touches like truncating long note titles replacing the remainder of the title with an ellipsis and adding a MessageDialog to ask the user to confirm an operation:
To begin, I’ll open the New Project dialog, and (1) create a new Blank App project template (2) called MapNotes, then (3) click the OK button:
First, I’ll add a DataModel folder:
Then will add a new file to that folder. In the Add New Item dialog, I’ll add (1) a class (2) called DataSource.cs, then (3) I’ll click the Add button:
In the new DataSource.cs file, I’ll create a model called MapNote, adding a number of properties that represent the note, where the note was created, etc.:
The DataSource class will be our primary focus. You’ll notice how similar it is to other DataSource classes we created in the past. In fact, as I was developing this, I merely copied code and renamed the data class to MapNote (since that is the type of data we’re working with in this app). While I don’t advocate copy and paste always, as a means of templating some common functionality, it can be useful.
I’ll start by creating a private ObservableCollection<MapNote> that the remainder of the methods in the DataSource class will manage… adding new items, deleting items, saving and retrieving from Phone storage, etc.:
We’ll begin by providing access to the _mapNotes through the GetMapNotes() method. Again, note the templated nature of this method from what we’ve used earlier. GetMapNotes() called a helper method called ensureDataLoaded(). The ensureDataLoaded() method checks the count of items in the collection, and if it is empty will call the getMapNoteDataAsync() which has the responsibility of hydrating the object graph of MapNotes from Phone storage into memory.
We’ve talked about methods similar to getMapNoteDataAsync() in the past. In a nut shell, we’ll use a DataContractJsonSerializer to open a file from the Phone’s LocalFolder storage and de-serialize the file’s contents into an in-memory collection of MapNotes.
We’ll need to create a constant with the fileName since we’ll be using it to both save and retrieve data on disk.
We want to allow the user to create new MapNote objects and save them to the Phone’s storage. The AddMapNote() method will add a new MapNote to the private ObservableCollection<MapNote>, then persist that to storage by calling saveMapNoteDataAsync(). The saveMapNoteDataAsync() is the corollary to getMapNoteDataAsync() … it, too, uses a DataContractJsonSerializer to save a file to the Phone’s Local Folder.
Finally, we want to allow a user to delete a note. We’ll call Remove() on the collection of MapNotes, then again, call saveMapNotDataAsync() to make sure that change is saved to the Phone’s storage:
We want to make our DataSource class available throughout all pages in our app, so I decided to create an instance of DataSource as a public static field on the App claass. In the App’s constructor, I create an instance of DataSource and set it to the DataModel field:
Now, we’re ready to create a page that will allow a user to add a new note or look at an existing note. While looking at an existing note, we’ll allow the user to delete the note as well. We’ll use a single page for both purposes, changing out the names of the buttons and their operation depending on the current state of the current MapNote.
I’ll add a new item, a new Blank Page called AddMapNote.xaml:
Before I begin to lay out the controls on the new AddMapNote.xaml page, I need a way to navigate from the MainPage.xaml to the AddMapNote.xaml page. There will be two ways to do this … the first will be to click a button in the MainPage.xaml’s CommandBar to add a new note. The second way will be to tap an existing note’s title / entry in the list of notes on that page to view (and potentially delete) that note.
First, I want to add a CommandBar and CommandBarButton. I’ll put my mouse cursor in the Page definition, and in the Properties window I’ll click the New button next to BottomAppBar:
That action will create a Page.BottomAppBar and a CommandBar. Now, I’ll put my mouse cursor in the CommandBar and then select the ellipsis button next to PrimaryCommandButton:
And the PrimaryCommandButton editor dialog appears.
I’ll (1) click the Add button to add a new AppBarButton, (2) select the newly added button in the Items list box on the left, then (3) make sure to change the Icon to type SymbolIcon, and (4) select the Map symbol from the dropdown list:
(1) I’ll scroll down to the Label property and set that to “Add Note”, then (2) click the OK button:
Finally, in the XAML editor, I want to add a click event handler for the new button, so I’ll add a Click=”AppBarButton_Click” event handler method.
I’ll use the F12 technique to create the method stub in the MainPage.xaml.cs file. Here, I’ll want to set the DataContext for the page to the collection of MapNotes in the OnNavigatedTo() method (like we’ve demonstrated before). I’ll also Frame.Navigate() to AddMapNote.xaml when the user clicks the AppBarButton:
Back on the MainPage.xaml, I’ll lay out the main area of the page by (1) surrounding the Grid with a ScrollViewer so that we can view a long list of items, (2) I’ll add two RowDefinition objects to create an area for the app title and for the list of items, (3) I’ll add a TextBlock with the title of the app into that first RowDefinition with the style set to the built-in HeaderTextBlockStyle:
I’ll add a ListView control into the second RowDefinition, binding its ItemsSource to the Page’s DataContext, and setting properties to ensure that a given list item can be tapped, not selected. This involves setting the SelectionMode to none, the IsItemClickEnabled to true, and creating an event handler for the ItemClick event. Furthermore, I’ll add the ItemTemplate and DataTemplate so that we can begin fleshing those out in the next step:
Each list view item will be comprised of two TextBlocks … one bound to the Title of the note, the other to the Note property of the MapNote:
Now, we’ll move on to the AddMapNote.xaml page. Here, I’ll add four row definitions and set the margins on the Grid:
<Grid Margin="10,0,10,0">
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
</Grid>
I’ll leave the top row empty in order to give the app some vertical spacing. I suppose I could add my app’s title or other branding elements there as well. In the second row, I’ll add a StackPanel containing the Title and Note TextBoxes:
<StackPanel Grid.Row="1">
<TextBlock Text="Title:" />
<TextBox x:Name="titleTextBox" TextWrapping="Wrap" />
<TextBlock Text="Note:"/>
<TextBox x:Name="noteTextBox" TextWrapping="Wrap" Height="125" />
</StackPanel>
Next, I’ll drag-and-drop a Map Control to the desginer which adds the MapControl and its namespace to the Page. I’ll clean out the extraneous properties and set it to the third row:
<Maps:MapControl x:Name="MyMap" Grid.Row="2" />
In the fourth row, I’ll add two buttons in a StackPanel along with event handler methods for their Click events:
<StackPanel Orientation="Horizontal" Grid.Row="3">
<Button x:Name="addButton" Content="Add" Click="addButton_Click" Margin="0,0,10,0" />
<Button x:Name="cancelButton" Content="Cancel" Click="cancelButton_Click" />
</StackPanel>
When I’m finished, the preview looks like this:
While the name of the page is AddMapNote.xaml, I intend to use it for two scenarios: adding a MapNote and viewing / deleting a MapNote. To accomplish this I’ll need to often determine the current scenario I’m in … am I adding or viewing / deleting?
We see how that affects the OnNavigatedTo() event handler … if I’m adding a new MapNote, then I’ll want to determine the current location of the Phone. However, if I’m viewing / deleting a MapNote, I’ll want to retrieve the location from the existing MapNote and set the Map Control to display it on the Page.
I’ll start with the “Add” scenario. I’ll use the Geolocator object to determine the current position:
I still have a lot of work to do here, such as set the Map Control to the current Geopoint, however this is a good start.
The note reminds me I need to add a Capability for Location. I’ll open the Package.appxmanifest file, go to the Capabilities page, and click the check box next to Location:
Back in the AddMapNote.xaml, I’ll handle the “Add” scenario for the addButton_Click() event handler method. I’ll need to resolve some namespaces, and I’ll need to fix a problem with the names of one of my properties which I misspelled (will do that in the next step). Here, we’ll create a new instance of the MapNote class, set its properties including the Latitude and Longitude, then call the App.DataModel.AddMapNote() passing in the new instance of MapNote. Finally, I’ll navigate back to the MainPage.xaml:
As I said a moment ago, I realize I forgot the letter ‘d’ on the property named Created. I’ll fix that back in the MapNote class:
Back in the AppNoteNote.xaml.cs, I want to think about the OnNavigatedTo() event handler again. This time, I want to think about how I’ll handle both the “add” and “view / delete” scenarios. I’ll use the NavigationEventArgs input parameter that contains the input parameter from the previous page. If the parameter is empty (null), that will signal the “add” scenario. Otherwise, it will signal the “view / delete” scenario. The objective of either case is to determine the Geopoint so that I can set the map to it:
I’ll copy and paste the code I previously had added to OnNavigatedTo() into the “add” case:
Then in the “view / delete” case I’ll cast the NavigationEventArg’s Parameter property to MapNote and populate the textboxes. I’ll also create a Geopoint based on the latitude and longitude of the MapNote. Finally, outside of the if statement, I attempt to call MyMap.TrySetViewAsync() passing in the Geopoint that was created in either case, as well as a default zoom level:
I realize that when I click the Add button, I’ll need some way to determine whether we’re currently in the “add” or “view / edit” scenario, so I create a bool flag called isViewing:
In the “add” scenario, I set it to false:
In the “view / delete” scenario, I set it to true. Now I can tell what scenario I’m handling anywhere in the AddMapNote.xaml.cs:
I’ll use the isViewing flag to branch the logic of what happens in each scenario:
I’ll clean up a little by removing an extraneous Frame.Navigate():
If we’re currently in the “view / delete” scenario, I’ll need to first delete the MapNote, then navigate back to MainPage.xaml. However, I have a problem … I currently have now way of getting to the MapNote that was loaded into the page. This will require I step back a bit and create a private reference to the MapNote…
… here I add a private field of type MapNote. Now, when in the “view / delete” scenario, I’ll use that instead of my locally scoped variable of the same name:
Therefore, I’ll remove the var keyword in front of the mapNote like so:
When someone clicks the delete button, I want a popup dialog to ask the user if they’re sure they want to delete the MapNote. This will prevent an accidental deletion. To accomplish this, I add code to display a MessageDialog object. The MessageDialog will have two Commands (rendered as buttons) … “Delete” and “Cancel”. Regardless of which one the user clicks, both will trigger the execution of a handler method called “CommandInvokedHandler”. Finally, there are two lines of code that are unnecessary in this instance, but useful when creating a Windows Store app: I set the default command that should be executed when the user clicks the Esc key on their keyboard. Again, not pertinent here, but it won’t hurt and you can see how to implement that. Finally, once we’ve properly set up the MessageDialog we call ShowAsync() to display it to the user:
Of course, I’ll have to resolve namespaces by adding some using statements:
Next, I’ll implement the CommandInvokedHandler(). I’ll use the Label property of the command button that was clicked to determine which action to take, whether Cancel or Delete. I’m only interested in the Delete scenario. I’ll call DeleteMapNote() then Frame.Navigate():
I’ll need to clean up those two lines of code in the addbutton_Click() event handler method since I don’t need them any more. In the screenshot below, I removed them both from the “view / delete” scenario:
Also, to resolve all of the compilation errors, I’ll need to add the async keyword since I’m calling a method that can be awaited:
When I’m finished, this should be the result … the entire code listing for AddMapNote.xaml.cs:
using MapNotes.DataModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Devices.Geolocation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace MapNotes
{
public sealed partial class AddMapNote : Page
{
private bool isViewing = false;
private MapNote mapNote;
public AddMapNote()
{
this.InitializeComponent();
}
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
Geopoint myPoint;
if (e.Parameter == null)
{
// Add
isViewing = false;
var locator = new Geolocator();
locator.DesiredAccuracyInMeters = 50;
// MUST ENABLE THE LOCATION CAPABILITY!!!
var position = await locator.GetGeopositionAsync();
myPoint = position.Coordinate.Point;
}
else
{
// View or Delete
isViewing = true;
mapNote = (MapNote)e.Parameter;
titleTextBox.Text = mapNote.Title;
noteTextBox.Text = mapNote.Note;
addButton.Content = "Delete";
var myPosition = new Windows.Devices.Geolocation.BasicGeoposition();
myPosition.Latitude = mapNote.Latitude;
myPosition.Longitude = mapNote.Longitude;
myPoint = new Geopoint(myPosition);
}
await MyMap.TrySetViewAsync(myPoint, 16D);
}
private async void addButton_Click(object sender, RoutedEventArgs e)
{
if (isViewing)
{
// Delete
var messageDialog = new Windows.UI.Popups.MessageDialog("Are you sure?");
// Add commands and set their callbacks; both buttons use the same callback function instead of inline event handlers
messageDialog.Commands.Add(new UICommand(
"Delete",
new UICommandInvokedHandler(this.CommandInvokedHandler)));
messageDialog.Commands.Add(new UICommand(
"Cancel",
new UICommandInvokedHandler(this.CommandInvokedHandler)));
// Set the command that will be invoked by default
messageDialog.DefaultCommandIndex = 0;
// Set the command to be invoked when escape is pressed
messageDialog.CancelCommandIndex = 1;
// Show the message dialog
await messageDialog.ShowAsync();
}
else
{
// Add
MapNote newMapNote = new MapNote();
newMapNote.Title = titleTextBox.Text;
newMapNote.Note = noteTextBox.Text;
newMapNote.Created = DateTime.Now;
newMapNote.Latitude = MyMap.Center.Position.Latitude;
newMapNote.Longitude = MyMap.Center.Position.Longitude;
App.DataModel.AddMapNote(newMapNote);
Frame.Navigate(typeof(MainPage));
}
}
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
Frame.Navigate(typeof(MainPage));
}
private void CommandInvokedHandler(IUICommand command)
{
if (command.Label == "Delete")
{
App.DataModel.DeleteMapNote(mapNote);
Frame.Navigate(typeof(MainPage));
}
}
}
}
The app should work as I test all the scenarios of adding, viewing and deleting a MapNote as I move the Emulator’s location around the world.
The final change I want to make is to account for the possibility that a given note has a very long title. Currently, the letters will disappear off the right-hand side or possibly wrap to the next line (unless you set the height of the TextBlock):
At the very least, I can add a few properties to prevent both wrapping AND trim the final letters that will appear off screen using the TextWrapping and TextTrimming properties, respectively:
I’ll also add the TextWrapping property to the Note TextBlock:
Now, my MapNotes will appear correctly on screen:
Another successful Exercise. Hopefully this exercise was helpful in cementing many of the ideas we’ve already covered. At a minimum, you saw how we were able to reuse a lot of the DataSource / Data Model code. If your data model is flat (i.e., not a deep hierarchy of relationships between classes), you now have a good recipe / template that you can re-use. It allows you to store your object graph to the Phone’s storage and grab it back out. All you have to do is create a new class that can be serialized. Even if you have Commands defined you can add the IgnoreDataMemberAttribute to those members that you do not want to be serialized.
We also looked at how to utilize the Map Control, how to change its Geoposition using the Geolocator and how to create an instance of a Geopoint using a latitude, longitude, how to display a MessageDialog and handle the callback function and much more.
I wanted to test and see how this worked on my phone, but no xap file is created after building.
Couldn't make the "Delete" command work at all :(
It doesn't delete the note
Hey,
I've been following along and have successfully created the mapNotes app with your guidance. I've learned a lot just by following through the lessons.
However, I would like to go above and beyond and create a slightly different version of this app for myself. With that being said, I'm currently stuck on this other project. I am simply trying to make this app so that it will automatically record and log GPS points in a background task, that way the application doesn't have to be on the screen for it to be automatically recording points and logging them to a list.
How would I go about doing this? I've been trying really hard to research background tasks for windows phone 8.1 and the info I'm coming across is very confusing and hard to implement in my own way.
Please help!!
Thanks for reading,
Justin