Now we have a map and a single image returned from Flickr, we have the basic building blocks for our app. But our eventual aim is to show a list of images instead of just one. So, in this lesson we'll do that by adding a SearchResults page and displaying all the images returned by our search.
Our game plan in this lesson:
1. We'l add the AroundMe assets for the tile image and so on
2. We'l add an Application Bar and Search button that wil navigate to a new page that wil ultimately display Flickr images, sending along the latitude and longitude as parameters
3. We'l add a new page, the SearchResults.xaml page, we'l add a text block to the page, and we'l add code that retrieves the parameters sent from the MainPage to the new SearchResults page and display the latitude and longitude values, just to make sure we are correctly passing that data
4. We'l clean up and refactor the code we've written into class files, and clean up the code that calls our new class file methods so that it more closely resembles our original diagrams / low-tech mock ups of the user interface and user interaction
1. Add AroundMe assets to project
In the downloads available for this series, there's a subfolder called AroundMe_Assets.
We'll be copying (dragging and dropping) the files and subfolders in that directory to
Visual Studio to make them part of our project's Assets.
First, delete everything inside of the Assets folder ...
... then in Windows Explorer, drag and drop the files in the AroundMe_Assets folder to
Visual Studio's Solution Explorer beneath the Assets folder. When you're finished it
should look like this:
The number of files may look the same, but their contents are different ... i.e., the
ApplicationIcon.png is for the AroundMe application. There's also an icon called
feature.search.png ... it will be used in this lesson as the icon for the Application Bar's
Search button.
2. Make sure assets appear in the WMAppManifest.xml correctly
The assets (images) we copied into our project's Assets folder should match the names
of the default assets added by the project template. Just to make sure, let's take a look
at the WMAppManifest.xml and see if the new image assets appear as expected.
Find the WMAppManifest.xml file in the Properties folder of the project. Double-click to
open:
In the WMAppManifest.xml designer, the App Icon should have the appearance of the
skyline of my home town (and Clint's home town), Chicago.
Furthermore, we'll want to make sure that Tile Template is set to TemplateCycle, and
that the Tile Images (Small, Cycle 1) look like the App Icon:
3. Add an Application Bar and Search Button
We've added an Application Bar in a previous lesson, so much of this should look
familiar:
1. I'l create a new instance of the ApplicationBar class
2. I'l create a new instance of the ApplicationBarIconButton class ... in the constructor, I'l reference the location of the Search icon we added a moment ago.
3. I'l add the text that wil be displayed under the button, and attach the SearchClick event handler method to the Click event of the button. Obviously, that method doesn't exist just yet. We'l come back to this in a moment and implement it.
4. I'l add the new Search button to the Application Bar.
Now, let's go back and implement the SearchClick method (from #3, above). There's a
little blue bar under the 'S' in 'SearchClick'. Hover your mouse cursor until a tiny menu
appears. It will drop down to another menu option to generate a method stub for the
SearchClick method:
If you click that menu option, it will generate a method stub as promised. We'll
implement that method in the next step...
4. Navigate to a new page, pass data to the page
I add the following code to the event handler method:
1. In this line, we create a string that represents the path to a page called SearchResults.xaml. We've not yet added that page to our application -- we'l do that in a moment. If you look at the string we're building, we're passing additional information in the form of a query string. We've already talked about query strings and passing data in a previous lesson. The only wrinkle is that this time we're sending two bits of data from the MainPage.xaml to a new page we've yet to create called SearchResults.xaml. You use the ampersand character & to separate name / value pairs in a query string. Additionally, we're using String.Format() to substitute the actual latitude and longitude values into the Uri string.
2. In line 140, we're using a class called the NavigationManager to Navigate() from the MainPage.xaml to a new page as defined in the new Uri object. The NavigationService not only knows how to navigate from one page to another, but is also responsible for keeping a history of previous navigations so that, when you use the back button on your Windows Phone 8, it can take you back to both the page AND the state of that page as you left it previously.
Now that we've created the Application Bar's Search button and have implemented the
Click event handler for that button, we'll want to display the Application Bar.
5. Show the Application Bar
We'll merely add a call to the BuildLocalizedApplicationBar() method we created a
moment ago.
Let's test the app:
... and now at the bottom of our app, we can see the Search icon. Clicking the ellipsis in
the upper-right hand corner of the App Bar, we can see it expand to show the Text
property of the button as well.
If we were to click it, the NavigationService.Navigate() method would probably throw an
exception because we do not have a page called SearchResults,xaml in our project.
Let's add it.
6. Add a new item to the project, call it SearchResults.xaml
Right-click project name in Solution Explorer, select Add | New Item ... In the Add New
Item dialog:
1. Make sure you're in the Visual C# templates
2. Select Windows Phone Portrait Page
3. Make sure you call this: SearchResults.xaml
4. Click Add
Next, we'll want to quickly test to make sure the code we wrote correctly navigates to
our new page AND sends along those query string values.
7. Validate that the values are being passed correctly between the MainPage and
SearchResults page
In the content panel of the SearchResults.xaml page, I've removed everything, then
added a TextBlock control and named it LocationTextBlock.
Now, I'll write C# code to retrieve the values from the query string and display them in
the LocationTextBlock ... just to make I can grab those values that were passed in the
query string.
1. I create two private fields (variables) to hold the values I'l retrieve from the query string.
2. The OnNavigatedTo() event wil fire after the page class has been instantiated, but before the Page_Loaded event fires. This is a perfect opportunity to process the query string. I use the NavigationContext class' Query string property array to get at the individual items in the query string by their name. I convert their data type from string to Double.
3. I wire up the Loaded event for this page.
4. When the Loaded event fires, I set the Text value Formatting in the latitude and longitude.
Does it work?
Great!
Next, I'll take my first step towards refactoring the code I've written. I'm using that term
(refactor) loosely in this context to mean I'm going to re-organize the code more
logically. Refactoring is the process of improving the quality (readability, structure,
organization, etc.) of the code without changing its function.
8. Refactor the classes into a separate file
At the moment, I have code in places it should not be. For example, I've implemented
three classes:
· Photo
· Photos
· FlickrData
... at the bottom of my MainPage.xaml.cs. We can do better than that.
Right-click on the project name, select Add | New Item ... from the context menu. The
Add New Item dialog appears ...
1. Make sure you're in the Visual C# item templates
2. Select the Class item template
3. Change the name to: Photo.cs
4. Click Add
Next, open the MainPage.xaml.cs file, highlight the Photo, Photos and FlickrData class
definitions ...
... cut them out of that file, and then paste them into the new Photo.cs file.
Photo.cs should look like this:
If this were a real project, I would probably separate each of these into their own files. I
do this as a convention for consistency ... each class should be in it's own file so that I
can easily locate every class in my project via the Solution Explorer. But that is merely a
stylistic preference of mine. This will work just fine.
Next, I'm going to implement a class that will wrap the call to the Flickr API web service
call.
9. Implement the FlickrImage.cs class
Right-click the project name in the Solution Explorer, select Add | New Item ... from the
context menu. When the Add New Item dialog appears ...
1. Make sure the Visual C# item templates are selected
2. Select the Class item template
3. Change the name to: FlickrImage.cs
4. Click Add
Next will involve writing a lot of code. I'll explain it after the code passage below:
The FlickrImage class has a dual purpose. (a ) An instance of this class represents a
single image's Flickr URIs at different image resolutions, and (b ) It has a public static
method (GetFlickrImages()) that knows how to call Flickr's API and retrieve a generic
List<FlickrImage>. Perhaps it would be a good idea to separate these into two separate
classes and class files, but I'll leave it as is for now.
1. I implement the two instance member properties representing the URIs pointing to two sizes of each Flickr image we'l use in this project. The smaller image wil be used in the SearchResults page, and the larger image will be used on the lock screen much later. The rest of this code passage is the public static method that wil create a List<FlickrImage> by calling Flickr's API. We moved most of this code from the MainPage.xaml.cs UpdateMap() method to its new home here.
2. This public static method returns a Task<List<FlickrImage>> ... what is this Task<> that surrounds the List<FlickrImage>? I'm going to devote a whole lesson to talking about the async features we're using in this app, and explain the new Async features in C#. It's the reason behind the Task<> keyword as well as the async keyword. For now, I just ask that you trust me ... it needs to be there and I'l explain why later. The method's signature requires the Flickr API key is passed in along with a latitude and longitude input parameter each defaulted to double.NaN, which means, Not a Number. In other words, if the caller of this method doesn't pass in a value, we don't want it defaulting to 0 because 0 represents a real coordinate. We'd rather use NaN and check for that later to ensure that we are in fact working with a real number or not. We'l implement that check later as we try to harden our application against potential problems.
3. Working with Flickr's API requires a URL complete with query string parameter values. I foresee that the process of creating that query string can be messy, so I'm going to remove that and put it in a helper method that I'l implement later in this lesson. To construct that URL I'l need many of the same parameters that were passed into this method, so I pass them along as input parameters to the soon-to-be-implemented getBaseUrl() method. By moving the complex string manipulation required to call the search API into it's own helper method, it keeps this method more readable.
4. We want to convert the Photo class instances into the new instances of the FlickrImage class since this is just the information we'l need to display in our results. In other words, we only real y need to know the two sizes of images ... one for the smaller thumbnails and one for the actual lock screen on our phone. We'l need to build URLs for these two versions of the image based on other information provided to us in the Photo class. So, we're basically just simplifying the data returned back to us by creating a new, simplified, streamlined list of objects, each with two properties.
5. Here we set the two properties of our new FlickrImage object to URIs. The suffix identifies the two versions / sizes of the photos we want to download.
6. Finally, we'l return the List<FlickrImages> to the caller.
Let's circle back and revisit callout #3 (above) ... we wanted to separate all the complex
string manipulation logic required to construct a URL for the search API query to it's own
private helper method called getBaseUrl().
Hopefully nothing here requires much explanation. We're merely moving this
functionality out of our MainPage.xaml.cs's unwieldy UpdateMap() method to a new location. We're hiding all this complex string manipulation behind a friendly method
name to make the code more readable.
IMPORTANT: I did make two significant changes here due to small bugs that I
discovered in the original code.
1. I replaced:
license.Replace(",", "%2C");
... with ...
license = license.Replace(",", "%2C");
... because the Replace method returns a string. It does not merely operate and set the
string it is called on.
2. The second problem wil only surface if you attempt to use this app on your physical Phone device, or perhaps if you choose a different location than I've selected in the Emulator. Actual y, it's not a problem with our app, per se, it's a problem with Flickr's API. It wil only accept 5 digits of precision after the decimal point. If you send it more than 5 digits of precision, it wil never return a result. So, we need to use the Math.Round() method to ensure that we only send up to 5 digits, or rather, places of precision.
8. Refactor the layout and display logic
Now the UpdateMap() method can be greatly simplified because we've offloaded the
responsibility of displaying results to the SearchResults.xaml.cs page.
I'll remove everything except just the code required to update the map with the current
GPS position of the phone:
Next, I can remove the (temporary) XAML controls I was using to check my results ...
namely, I remove the Image control named FlickrImage and the TextBlock control
named ResultTextBlock.
Now I need to visit the SearchResults.xaml.cs page and wire it up to:
1. retrieve the latitude and longitude from the query string once the page has been navigated to
2. kick off the search to Flickr once the page has been loaded.
1. Rather than hard code it in the FlickrImage class, I decided to move the FlickrApiKey out to the SearchResults.xaml.cs file as a constant. The reason is so that I could potentially re-use the FlickrImage class again later in another app. I would need a different FlickApiKey. It makes sense to pass that in at runtime.
2. I comment out the outdated reference to the LocationTextBlock. We no longer need that. It was just to help us get our bearings, but we're ready to move past that. Later, I'l delete these comments.
3. Here I make the call to FlickrImage.GetFlickrImages(). This kicks off the search. It's awaited, meaning our code is calling an await-able / async method. The flow of code can proceed, as long as we don't need the results (i.e., the images) from this call.
4. We set the DataContext for the SearchResults.xaml page. We'l use this as we bind a LongListSelector to the thumbnail images returned from line 37.
Finally, we'll add a LongListSelector, style its DataTemplate and set its bindings.
1. The SearchResults.xaml page wil mostly be comprised of a LongListSelector bound to the DataContext, i.e., the result of our call to FlickrImage.GetFlickrImage(), which wil be a List<FlickrImage>. The FlickrImage has two properties ... one representing the Uri for the thumbnail, and one representing the larger image (that we'l use for the lock screen). This LongListSelector will be in Grid layout mode, with each cell 105 x 105. We're binding to the DataContext of the page, so no need for additional information beyond the binding expression {Binding}.
2. Here we build the content for each cell of the LongListSelector grid ... an Image control in each cell. It wil bind to the Image320 property of the DataSource (which was a List<FlickrImage>, so we're binding to the FlickrImage.Image320 property).
When we run the app ...
... and perform a search by clicking the magnifying glass icon ...
We see results in or around the John Hancock Center. Awesome!
Recap
To quickly recap, the big takeaway in this lesson was navigating to a new page, how
restructured and refactored the code so that its implementation is cleaner and more
obvious -- you'll see the value of this reorganization as we continue this series and re-
use this code from other places. I'm really just planning ahead a little. While I don't think
we learned anything new with regards to binding and such, we did get another chance
to exercise things that we learned when working on the SoundBoard again.
No comments:
Post a Comment