Part 29 - Exercise: The Map Notes App

Download this episode

Download Video

Description

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. 

clip_image002

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:

clip_image004

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:

clip_image006

First, I’ll add a DataModel folder:

clip_image008

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:

clip_image010

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.:

clip_image012

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.:

clip_image014

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.

clip_image016

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.

clip_image018

We’ll need to create a constant with the fileName since we’ll be using it to both save and retrieve data on disk.

clip_image020

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.

clip_image022

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:

clip_image024

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:

clip_image026

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:

clip_image028

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:

clip_image030

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:

clip_image032

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:

clip_image034

(1) I’ll scroll down to the Label property and set that to “Add Note”, then (2) click the OK button:

clip_image036

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.

clip_image038

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:

clip_image040

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:

clip_image042

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:
clip_image044
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:

clip_image046

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:

clip_image048

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:

clip_image050

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:

clip_image052

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:

clip_image054

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:

clip_image056

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:

clip_image058

I’ll copy and paste the code I previously had added to OnNavigatedTo() into the “add” case:

clip_image060

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:

clip_image062

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:

clip_image064

In the “add” scenario, I set it to false:

clip_image066

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:

clip_image068

I’ll use the isViewing flag to branch the logic of what happens in each scenario:

clip_image070

I’ll clean up a little by removing an extraneous Frame.Navigate():

clip_image072

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…

clip_image074

… 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:

clip_image076

Therefore, I’ll remove the var keyword in front of the mapNote like so:

clip_image078

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:

clip_image080

Of course, I’ll have to resolve namespaces by adding some using statements:

clip_image082

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():

clip_image084

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:

clip_image086

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:

clip_image088

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):

clip_image090

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:

clip_image092

I’ll also add the TextWrapping property to the Note TextBlock:

clip_image094

Now, my MapNotes will appear correctly on screen:

clip_image096

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.

Embed

Format

Available formats for this video:

Actual format may change based on video formats available and browser capability.

    The Discussion

    • User profile image
      JackJ

      I wanted to test and see how this worked on my phone, but no xap file is created after building.

    • User profile image
      Pedro

      Couldn't make the "Delete" command work at all :(
      It doesn't delete the note

    • User profile image
      Justin B

      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

    Comments closed

    Comments have been closed since this content was published more than 30 days ago, but if you'd like to send us feedback you can Contact Us.