Click here to Skip to main content
13,146,014 members (42,687 online)
Click here to Skip to main content
Add your own
alternative version

Stats

2.7K views
3 bookmarked
Posted 12 Sep 2017

Walkthrough for Xamarin in VS2017 - Part Three

, 12 Sep 2017
Rate this:
Please Sign up or sign in to vote.
Part three continues extending and modernising functionality.
this.Title = default(string);
    this.Temperature = default(double);
    this.Wind = default(string);
    this.Humidity = default(string);
    this.Visibility = default(string);
    this.Sunrise = default(string);
    this.Sunset = default(string);
  DateTime time = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);

  return new Weather()
  {
    Title = (string)results["name"],
    Temperature = (string)results["main"]["temp"] + " F",
    Wind = (string)results["wind"]["speed"] + " mph",
    Humidity = (string)results["main"]["humidity"] + " %",
    Visibility = (string)results["weather"][0]["main"],
    Sunrise = $"{time.AddSeconds((double)results["sys"]["sunrise"]).ToString()} UTC",
    Sunset = $"{time.AddSeconds((double)results["sys"]["sunset"]).ToString()} UTC"
  };
}
else
  return null;

Introduction

In Part Two (https://www.codeproject.com/Articles/1192180/Walkthrough-for-Xamarin-in-VS2017-Part-Two) we updated our App to use XAML rather than code behind.

In this article we are going to start extending our App to become a real program, rather than demo code.

I would recommend going into NuGet and making sure all of your packages have been updated (Toos, Nuget Package Manager, Manage NuGet Packages for Solution.  Updates tab).  In particular the Xamarin controls are getting frequent updates.  We are going to be working with the Picker (i.e. a ComboBox) which has only recently been updated to take advantage of Binding.

Adding Country

Not all of us live in the USA. The Weather website does allow for us to specify which Country we want the weather for as an optional parameter.

Unfortuantely the Weather website, does not have a method for getting a list of Countries  / Country codes, instead pointing us to the ISO standards ISO 3166 (https://www.openweathermap.org/current).  If you search for this ISO code you can find a list of Countries and the associated codes, which we will turn into a Dictionary.

Select the WeatherApp Project in the Solution Explorer.  Right-Click, then select Add, then Class and name your new class Country.cs:

using System.Collections.ObjectModel;

namespace WeatherApp
{
  public class Country
  {
    public string CountryCode;
    public string CountryName;

    //This returns the the CountryName (CountryCode) instead of WeatherApp.Country, which is used for display in the Country Picker drop-down.
    public override string ToString() => $"{CountryName} ({CountryCode})";

    public static ObservableCollection<Country> Init()
    {
      ObservableCollection<Country> CountryList = new ObservableCollection<Country>();
     
      CountryList.Add(new Country() { CountryCode = "AF", CountryName = "Afgahanistan"});
      CountryList.Add(new Country() { CountryCode = "AX", CountryName = "Åland Islands"});
      CountryList.Add(new Country() { CountryCode = "AL", CountryName = "Albania"});
      CountryList.Add(new Country() { CountryCode = "DZ", CountryName = "Algeria"});
      CountryList.Add(new Country() { CountryCode = "AS", CountryName = "American Samoa"});
      CountryList.Add(new Country() { CountryCode = "AD", CountryName = "Andorra"});
      CountryList.Add(new Country() { CountryCode = "AO", CountryName = "Angola"});
      CountryList.Add(new Country() { CountryCode = "AI", CountryName = "Anguilla"});
      CountryList.Add(new Country() { CountryCode = "AQ", CountryName = "Antarctica"});
      CountryList.Add(new Country() { CountryCode = "AG", CountryName = "Antigua and Barbuda"});
      CountryList.Add(new Country() { CountryCode = "AR", CountryName = "Argentina"});
      CountryList.Add(new Country() { CountryCode = "AM", CountryName = "Armenia"});
      CountryList.Add(new Country() { CountryCode = "AW", CountryName = "Aruba"});
      CountryList.Add(new Country() { CountryCode = "AU", CountryName = "Australia"});
      CountryList.Add(new Country() { CountryCode = "AT", CountryName = "Austria"});
      CountryList.Add(new Country() { CountryCode = "AZ", CountryName = "Azerbaijan"});
      CountryList.Add(new Country() { CountryCode = "BS", CountryName = "Bahamas"});
      CountryList.Add(new Country() { CountryCode = "BH", CountryName = "Bahrain"});
      CountryList.Add(new Country() { CountryCode = "BD", CountryName = "Bangladesh"});
      CountryList.Add(new Country() { CountryCode = "BB", CountryName = "Barbados"});
      CountryList.Add(new Country() { CountryCode = "BY", CountryName = "Belarus"});
      CountryList.Add(new Country() { CountryCode = "BE", CountryName = "Belgium"});
      CountryList.Add(new Country() { CountryCode = "BZ", CountryName = "Belize"});
      CountryList.Add(new Country() { CountryCode = "BJ", CountryName = "Benin"});
      CountryList.Add(new Country() { CountryCode = "BM", CountryName = "Bermuda"});
      CountryList.Add(new Country() { CountryCode = "BT", CountryName = "Bhutan"});
      CountryList.Add(new Country() { CountryCode = "BO", CountryName = "Bolivia, Plurinational State of"});
      CountryList.Add(new Country() { CountryCode = "BQ", CountryName = "Bonaire, Sint Eustatius and Saba"});
      CountryList.Add(new Country() { CountryCode = "BA", CountryName = "Bosnia and Herzegovina"});
      CountryList.Add(new Country() { CountryCode = "BW", CountryName = "Botswana"});
      CountryList.Add(new Country() { CountryCode = "BV", CountryName = "Bouvet Island"});
      CountryList.Add(new Country() { CountryCode = "BR", CountryName = "Brazil"});
      CountryList.Add(new Country() { CountryCode = "IO", CountryName = "British Indian Ocean Territory"});
      CountryList.Add(new Country() { CountryCode = "BN", CountryName = "Brunei Darussalam"});
      CountryList.Add(new Country() { CountryCode = "BG", CountryName = "Bulgaria"});
      CountryList.Add(new Country() { CountryCode = "BF", CountryName = "Burkina Faso"});
      CountryList.Add(new Country() { CountryCode = "BI", CountryName = "Burundi"});
      CountryList.Add(new Country() { CountryCode = "KH", CountryName = "Cambodia"});
      CountryList.Add(new Country() { CountryCode = "CM", CountryName = "Cameroon"});
      CountryList.Add(new Country() { CountryCode = "CA", CountryName = "Canada"});
      CountryList.Add(new Country() { CountryCode = "CV", CountryName = "Cape Verde"});
      CountryList.Add(new Country() { CountryCode = "KY", CountryName = "Cayman Islands"});
      CountryList.Add(new Country() { CountryCode = "CF", CountryName = "Central African Republic"});
      CountryList.Add(new Country() { CountryCode = "TD", CountryName = "Chad"});
      CountryList.Add(new Country() { CountryCode = "CL", CountryName = "Chile"});
      CountryList.Add(new Country() { CountryCode = "CN", CountryName = "China"});
      CountryList.Add(new Country() { CountryCode = "CX", CountryName = "Christmas Island"});
      CountryList.Add(new Country() { CountryCode = "CC", CountryName = "Cocos (Keeling) Islands"});
      CountryList.Add(new Country() { CountryCode = "CO", CountryName = "Colombia"});
      CountryList.Add(new Country() { CountryCode = "KM", CountryName = "Comoros"});
      CountryList.Add(new Country() { CountryCode = "CG", CountryName = "Congo"});
      CountryList.Add(new Country() { CountryCode = "CD", CountryName = "Congo, the Democratic Republic of the"});
      CountryList.Add(new Country() { CountryCode = "CK", CountryName = "Cook Islands"});
      CountryList.Add(new Country() { CountryCode = "CR", CountryName = "Costa Rica"});
      CountryList.Add(new Country() { CountryCode = "CI", CountryName = "Côte d'Ivoire"});
      CountryList.Add(new Country() { CountryCode = "HR", CountryName = "Croatia"});
      CountryList.Add(new Country() { CountryCode = "CU", CountryName = "Cuba"});
      CountryList.Add(new Country() { CountryCode = "CW", CountryName = "Curaçao"});
      CountryList.Add(new Country() { CountryCode = "CY", CountryName = "Cyprus"});
      CountryList.Add(new Country() { CountryCode = "CZ", CountryName = "Czech Republic"});
      CountryList.Add(new Country() { CountryCode = "DK", CountryName = "Denmark"});
      CountryList.Add(new Country() { CountryCode = "DJ", CountryName = "Djibouti"});
      CountryList.Add(new Country() { CountryCode = "DM", CountryName = "Dominica"});
      CountryList.Add(new Country() { CountryCode = "DO", CountryName = "Dominican Republic"});
      CountryList.Add(new Country() { CountryCode = "EC", CountryName = "Ecuador"});
      CountryList.Add(new Country() { CountryCode = "EG", CountryName = "Egypt"});
      CountryList.Add(new Country() { CountryCode = "SV", CountryName = "El Salvador"});
      CountryList.Add(new Country() { CountryCode = "GQ", CountryName = "Equatorial Guinea"});
      CountryList.Add(new Country() { CountryCode = "ER", CountryName = "Eritrea"});
      CountryList.Add(new Country() { CountryCode = "EE", CountryName = "Estonia"});
      CountryList.Add(new Country() { CountryCode = "ET", CountryName = "Ethiopia"});
      CountryList.Add(new Country() { CountryCode = "FK", CountryName = "Falkland Islands (Malvinas)"});
      CountryList.Add(new Country() { CountryCode = "FO", CountryName = "Faroe Islands"});
      CountryList.Add(new Country() { CountryCode = "FJ", CountryName = "Fiji"});
      CountryList.Add(new Country() { CountryCode = "FI", CountryName = "Finland"});
      CountryList.Add(new Country() { CountryCode = "FR", CountryName = "France"});
      CountryList.Add(new Country() { CountryCode = "GF", CountryName = "French Guiana"});
      CountryList.Add(new Country() { CountryCode = "PF", CountryName = "French Polynesia"});
      CountryList.Add(new Country() { CountryCode = "TF", CountryName = "French Southern Territories"});
      CountryList.Add(new Country() { CountryCode = "GA", CountryName = "Gabon"});
      CountryList.Add(new Country() { CountryCode = "GM", CountryName = "Gambia"});
      CountryList.Add(new Country() { CountryCode = "GE", CountryName = "Georgia"});
      CountryList.Add(new Country() { CountryCode = "DE", CountryName = "Germany"});
      CountryList.Add(new Country() { CountryCode = "GH", CountryName = "Ghana"});
      CountryList.Add(new Country() { CountryCode = "GI", CountryName = "Gibraltar"});
      CountryList.Add(new Country() { CountryCode = "GR", CountryName = "Greece"});
      CountryList.Add(new Country() { CountryCode = "GL", CountryName = "Greenland"});
      CountryList.Add(new Country() { CountryCode = "GD", CountryName = "Grenada"});
      CountryList.Add(new Country() { CountryCode = "GP", CountryName = "Guadeloupe"});
      CountryList.Add(new Country() { CountryCode = "GU", CountryName = "Guam"});
      CountryList.Add(new Country() { CountryCode = "GT", CountryName = "Guatemala"});
      CountryList.Add(new Country() { CountryCode = "GG", CountryName = "Guernsey"});
      CountryList.Add(new Country() { CountryCode = "GN", CountryName = "Guinea"});
      CountryList.Add(new Country() { CountryCode = "GW", CountryName = "Guinea-Bissau"});
      CountryList.Add(new Country() { CountryCode = "GY", CountryName = "Guyana"});
      CountryList.Add(new Country() { CountryCode = "HT", CountryName = "Haiti"});
      CountryList.Add(new Country() { CountryCode = "HM", CountryName = "Heard Island and McDonald Islands"});
      CountryList.Add(new Country() { CountryCode = "VA", CountryName = "Holy See (Vatican City State)"});
      CountryList.Add(new Country() { CountryCode = "HN", CountryName = "Honduras"});
      CountryList.Add(new Country() { CountryCode = "HK", CountryName = "Hong Kong"});
      CountryList.Add(new Country() { CountryCode = "HU", CountryName = "Hungary"});
      CountryList.Add(new Country() { CountryCode = "IS", CountryName = "Iceland"});
      CountryList.Add(new Country() { CountryCode = "IN", CountryName = "India"});
      CountryList.Add(new Country() { CountryCode = "ID", CountryName = "Indonesia"});
      CountryList.Add(new Country() { CountryCode = "IR", CountryName = "Iran, Islamic Republic of"});
      CountryList.Add(new Country() { CountryCode = "IQ", CountryName = "Iraq"});
      CountryList.Add(new Country() { CountryCode = "IE", CountryName = "Ireland"});
      CountryList.Add(new Country() { CountryCode = "IM", CountryName = "Isle of Man"});
      CountryList.Add(new Country() { CountryCode = "IL", CountryName = "Israel"});
      CountryList.Add(new Country() { CountryCode = "IT", CountryName = "Italy"});
      CountryList.Add(new Country() { CountryCode = "JM", CountryName = "Jamaica"});
      CountryList.Add(new Country() { CountryCode = "JP", CountryName = "Japan"});
      CountryList.Add(new Country() { CountryCode = "JE", CountryName = "Jersey"});
      CountryList.Add(new Country() { CountryCode = "JO", CountryName = "Jordan"});
      CountryList.Add(new Country() { CountryCode = "KZ", CountryName = "Kazakhstan"});
      CountryList.Add(new Country() { CountryCode = "KE", CountryName = "Kenya"});
      CountryList.Add(new Country() { CountryCode = "KI", CountryName = "Kiribati"});
      CountryList.Add(new Country() { CountryCode = "KP", CountryName = "Korea, Democratic People's Republic of"});
      CountryList.Add(new Country() { CountryCode = "KR", CountryName = "Korea, Republic of"});
      CountryList.Add(new Country() { CountryCode = "KW", CountryName = "Kuwait"});
      CountryList.Add(new Country() { CountryCode = "KG", CountryName = "Kyrgyzstan"});
      CountryList.Add(new Country() { CountryCode = "LA", CountryName = "Lao People's Democratic Republic"});
      CountryList.Add(new Country() { CountryCode = "LV", CountryName = "Latvia"});
      CountryList.Add(new Country() { CountryCode = "LB", CountryName = "Lebanon"});
      CountryList.Add(new Country() { CountryCode = "LS", CountryName = "Lesotho"});
      CountryList.Add(new Country() { CountryCode = "LR", CountryName = "Liberia"});
      CountryList.Add(new Country() { CountryCode = "LY", CountryName = "Libya"});
      CountryList.Add(new Country() { CountryCode = "LI", CountryName = "Liechtenstein"});
      CountryList.Add(new Country() { CountryCode = "LT", CountryName = "Lithuania"});
      CountryList.Add(new Country() { CountryCode = "LU", CountryName = "Luxembourg"});
      CountryList.Add(new Country() { CountryCode = "MO", CountryName = "Macao"});
      CountryList.Add(new Country() { CountryCode = "MK", CountryName = "Macedonia, the Former Yugoslav Republic of"});
      CountryList.Add(new Country() { CountryCode = "MG", CountryName = "Madagascar"});
      CountryList.Add(new Country() { CountryCode = "MW", CountryName = "Malawi"});
      CountryList.Add(new Country() { CountryCode = "MY", CountryName = "Malaysia"});
      CountryList.Add(new Country() { CountryCode = "MV", CountryName = "Maldives"});
      CountryList.Add(new Country() { CountryCode = "ML", CountryName = "Mali"});
      CountryList.Add(new Country() { CountryCode = "MT", CountryName = "Malta"});
      CountryList.Add(new Country() { CountryCode = "MH", CountryName = "Marshall Islands"});
      CountryList.Add(new Country() { CountryCode = "MQ", CountryName = "Martinique"});
      CountryList.Add(new Country() { CountryCode = "MR", CountryName = "Mauritania"});
      CountryList.Add(new Country() { CountryCode = "MU", CountryName = "Mauritius"});
      CountryList.Add(new Country() { CountryCode = "YT", CountryName = "Mayotte"});
      CountryList.Add(new Country() { CountryCode = "MX", CountryName = "Mexico"});
      CountryList.Add(new Country() { CountryCode = "FM", CountryName = "Micronesia, Federated States of"});
      CountryList.Add(new Country() { CountryCode = "MD", CountryName = "Moldova, Republic of"});
      CountryList.Add(new Country() { CountryCode = "MC", CountryName = "Monaco"});
      CountryList.Add(new Country() { CountryCode = "MN", CountryName = "Mongolia"});
      CountryList.Add(new Country() { CountryCode = "ME", CountryName = "Montenegro"});
      CountryList.Add(new Country() { CountryCode = "MS", CountryName = "Montserrat"});
      CountryList.Add(new Country() { CountryCode = "MA", CountryName = "Morocco"});
      CountryList.Add(new Country() { CountryCode = "MZ", CountryName = "Mozambique"});
      CountryList.Add(new Country() { CountryCode = "MM", CountryName = "Myanmar"});
      CountryList.Add(new Country() { CountryCode = "NA", CountryName = "Namibia"});
      CountryList.Add(new Country() { CountryCode = "NR", CountryName = "Nauru"});
      CountryList.Add(new Country() { CountryCode = "NP", CountryName = "Nepal"});
      CountryList.Add(new Country() { CountryCode = "NL", CountryName = "Netherlands"});
      CountryList.Add(new Country() { CountryCode = "NC", CountryName = "New Caledonia"});
      CountryList.Add(new Country() { CountryCode = "NZ", CountryName = "New Zealand"});
      CountryList.Add(new Country() { CountryCode = "NI", CountryName = "Nicaragua"});
      CountryList.Add(new Country() { CountryCode = "NE", CountryName = "Niger"});
      CountryList.Add(new Country() { CountryCode = "NG", CountryName = "Nigeria"});
      CountryList.Add(new Country() { CountryCode = "NU", CountryName = "Niue"});
      CountryList.Add(new Country() { CountryCode = "NF", CountryName = "Norfolk Island"});
      CountryList.Add(new Country() { CountryCode = "MP", CountryName = "Northern Mariana Islands"});
      CountryList.Add(new Country() { CountryCode = "NO", CountryName = "Norway"});
      CountryList.Add(new Country() { CountryCode = "OM", CountryName = "Oman"});
      CountryList.Add(new Country() { CountryCode = "PK", CountryName = "Pakistan"});
      CountryList.Add(new Country() { CountryCode = "PW", CountryName = "Palau"});
      CountryList.Add(new Country() { CountryCode = "PS", CountryName = "Palestine, State of"});
      CountryList.Add(new Country() { CountryCode = "PA", CountryName = "Panama"});
      CountryList.Add(new Country() { CountryCode = "PG", CountryName = "Papua New Guinea"});
      CountryList.Add(new Country() { CountryCode = "PY", CountryName = "Paraguay"});
      CountryList.Add(new Country() { CountryCode = "PE", CountryName = "Peru"});
      CountryList.Add(new Country() { CountryCode = "PH", CountryName = "Philippines"});
      CountryList.Add(new Country() { CountryCode = "PN", CountryName = "Pitcairn"});
      CountryList.Add(new Country() { CountryCode = "PL", CountryName = "Poland"});
      CountryList.Add(new Country() { CountryCode = "PT", CountryName = "Portugal"});
      CountryList.Add(new Country() { CountryCode = "PR", CountryName = "Puerto Rico"});
      CountryList.Add(new Country() { CountryCode = "QA", CountryName = "Qatar"});
      CountryList.Add(new Country() { CountryCode = "RE", CountryName = "Réunion"});
      CountryList.Add(new Country() { CountryCode = "RO", CountryName = "Romania"});
      CountryList.Add(new Country() { CountryCode = "RU", CountryName = "Russian Federation"});
      CountryList.Add(new Country() { CountryCode = "RW", CountryName = "Rwanda"});
      CountryList.Add(new Country() { CountryCode = "BL", CountryName = "Saint Barthélemy"});
      CountryList.Add(new Country() { CountryCode = "SH", CountryName = "Saint Helena, Ascension and Tristan da Cunha"});
      CountryList.Add(new Country() { CountryCode = "KN", CountryName = "Saint Kitts and Nevis"});
      CountryList.Add(new Country() { CountryCode = "LC", CountryName = "Saint Lucia"});
      CountryList.Add(new Country() { CountryCode = "MF", CountryName = "Saint Martin (French part)"});
      CountryList.Add(new Country() { CountryCode = "PM", CountryName = "Saint Pierre and Miquelon"});
      CountryList.Add(new Country() { CountryCode = "VC", CountryName = "Saint Vincent and the Grenadines"});
      CountryList.Add(new Country() { CountryCode = "WS", CountryName = "Samoa"});
      CountryList.Add(new Country() { CountryCode = "SM", CountryName = "San Marino"});
      CountryList.Add(new Country() { CountryCode = "ST", CountryName = "Sao Tome and Principe"});
      CountryList.Add(new Country() { CountryCode = "SA", CountryName = "Saudi Arabia"});
      CountryList.Add(new Country() { CountryCode = "SN", CountryName = "Senegal"});
      CountryList.Add(new Country() { CountryCode = "RS", CountryName = "Serbia"});
      CountryList.Add(new Country() { CountryCode = "SC", CountryName = "Seychelles"});
      CountryList.Add(new Country() { CountryCode = "SL", CountryName = "Sierra Leone"});
      CountryList.Add(new Country() { CountryCode = "SG", CountryName = "Singapore"});
      CountryList.Add(new Country() { CountryCode = "SX", CountryName = "Sint Maarten (Dutch part)"});
      CountryList.Add(new Country() { CountryCode = "SK", CountryName = "Slovakia"});
      CountryList.Add(new Country() { CountryCode = "SI", CountryName = "Slovenia"});
      CountryList.Add(new Country() { CountryCode = "SB", CountryName = "Solomon Islands"});
      CountryList.Add(new Country() { CountryCode = "SO", CountryName = "Somalia"});
      CountryList.Add(new Country() { CountryCode = "ZA", CountryName = "South Africa"});
      CountryList.Add(new Country() { CountryCode = "GS", CountryName = "South Georgia and the South Sandwich Islands"});
      CountryList.Add(new Country() { CountryCode = "SS", CountryName = "South Sudan"});
      CountryList.Add(new Country() { CountryCode = "ES", CountryName = "Spain"});
      CountryList.Add(new Country() { CountryCode = "LK", CountryName = "Sri Lanka"});
      CountryList.Add(new Country() { CountryCode = "SD", CountryName = "Sudan"});
      CountryList.Add(new Country() { CountryCode = "SR", CountryName = "Suriname"});
      CountryList.Add(new Country() { CountryCode = "SJ", CountryName = "Svalbard and Jan Mayen"});
      CountryList.Add(new Country() { CountryCode = "SZ", CountryName = "Swaziland"});
      CountryList.Add(new Country() { CountryCode = "SE", CountryName = "Sweden"});
      CountryList.Add(new Country() { CountryCode = "CH", CountryName = "Switzerland"});
      CountryList.Add(new Country() { CountryCode = "SY", CountryName = "Syrian Arab Republic"});
      CountryList.Add(new Country() { CountryCode = "TW", CountryName = "Taiwan, Province of China"});
      CountryList.Add(new Country() { CountryCode = "TJ", CountryName = "Tajikistan"});
      CountryList.Add(new Country() { CountryCode = "TZ", CountryName = "Tanzania, United Republic of"});
      CountryList.Add(new Country() { CountryCode = "TH", CountryName = "Thailand"});
      CountryList.Add(new Country() { CountryCode = "TL", CountryName = "Timor-Leste"});
      CountryList.Add(new Country() { CountryCode = "TG", CountryName = "Togo"});
      CountryList.Add(new Country() { CountryCode = "TK", CountryName = "Tokelau"});
      CountryList.Add(new Country() { CountryCode = "TO", CountryName = "Tonga"});
      CountryList.Add(new Country() { CountryCode = "TT", CountryName = "Trinidad and Tobago"});
      CountryList.Add(new Country() { CountryCode = "TN", CountryName = "Tunisia"});
      CountryList.Add(new Country() { CountryCode = "TR", CountryName = "Turkey"});
      CountryList.Add(new Country() { CountryCode = "TM", CountryName = "Turkmenistan"});
      CountryList.Add(new Country() { CountryCode = "TC", CountryName = "Turks and Caicos Islands"});
      CountryList.Add(new Country() { CountryCode = "TV", CountryName = "Tuvalu"});
      CountryList.Add(new Country() { CountryCode = "UG", CountryName = "Uganda"});
      CountryList.Add(new Country() { CountryCode = "UA", CountryName = "Ukraine"});
      CountryList.Add(new Country() { CountryCode = "AE", CountryName = "United Arab Emirates"});
      CountryList.Add(new Country() { CountryCode = "GB", CountryName = "United Kingdom"});
      CountryList.Add(new Country() { CountryCode = "US", CountryName = "United States"});
      CountryList.Add(new Country() { CountryCode = "UM", CountryName = "United States Minor Outlying Islands"});
      CountryList.Add(new Country() { CountryCode = "UY", CountryName = "Uruguay"});
      CountryList.Add(new Country() { CountryCode = "UZ", CountryName = "Uzbekistan"});
      CountryList.Add(new Country() { CountryCode = "VU", CountryName = "Vanuatu"});
      CountryList.Add(new Country() { CountryCode = "VE", CountryName = "Venezuela, Bolivarian Republic of"});
      CountryList.Add(new Country() { CountryCode = "VN", CountryName = "Viet Nam"});
      CountryList.Add(new Country() { CountryCode = "VG", CountryName = "Virgin Islands, British"});
      CountryList.Add(new Country() { CountryCode = "VI", CountryName = "Virgin Islands, U.S."});
      CountryList.Add(new Country() { CountryCode = "WF", CountryName = "Wallis and Futuna"});
      CountryList.Add(new Country() { CountryCode = "EH", CountryName = "Western Sahara"});
      CountryList.Add(new Country() { CountryCode = "YE", CountryName = "Yemen,YE"});
      CountryList.Add(new Country() { CountryCode = "ZM", CountryName = "Zambia"});
      CountryList.Add(new Country() { CountryCode = "ZW", CountryName = "Zimbabwe" });

      return CountryList;
    }
  }
}

Open Core.cs. and go to the async Task<Weather> GetWeather method.

First we need to update this function to take the additional Country parameter:

public static async Task<Weather> GetWeather(string pZipCode, string pCountryCode)
{

For backwards compatibility, we should not assume that the Country Code will be populated.  Check to see if the Country Code is empty or null and if so call the website as before.  IF the Country Code has been set, include this into the website call to get weather in another country:

if (String.IsNullOrEmpty(pCountryCode)) //If Country is not specified, Open Weather Defaults to the US.
  queryString += pZipCode + ",us&appid=" + key + "&units=imperial";
else
  queryString += pZipCode + "," + pCountryCode + "&appid=" + key + "&units=imperial";

The above code works, however it is a bit difficult to read.  We can update this syntax with the String Interpolation feature added in C# 6:

string queryString = default(String); //"http://api.openweathermap.org/data/2.5/weather?zip=";

if (String.IsNullOrEmpty(pCountryCode)) //If Country is not specified, Open Weather Defaults to the US.
  queryString = $"http://api.openweathermap.org/data/2.5/weather?zip={pZipCode},us&appid={key}&units=imperial";
else
  queryString = $"http://api.openweathermap.org/data/2.5/weather?zip={pZipCode},{pCountryCode}&appid={key}&units=imperial";

While we are cleaning up, update/replace the population of the value being returned:

1) Instead of creating a variable to hold the details in and then returning that value, simple create a new Weather() variable as part of the return statement.

2) instead of creating the variable, then populate it - combine the setting of the values in the creation statement. 

3) Finally, again use String Interpolation to simplfy the strings used for Sunrise and Sunset

if (results["weather"] != null)
{
  DateTime time = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);

  return new Weather()
  {
    Title = (string)results["name"],
    Temperature = (string)results["main"]["temp"] + " F",
    Wind = (string)results["wind"]["speed"] + " mph",
    Humidity = (string)results["main"]["humidity"] + " %",
    Visibility = (string)results["weather"][0]["main"],
    Sunrise = $"{time.AddSeconds((double)results["sys"]["sunrise"]).ToString()} UTC",
    Sunset = $"{time.AddSeconds((double)results["sys"]["sunset"]).ToString()} UTC"
  };
}
else
  return null;

Next we should clean update the properties.  Currently the GetWeather, Title, etc properties use hard-coded strings.  We update each of these strings (e.g. "Title") to use the nameof function (nameof(Title)).  In this way, if the property name changes in the future, data binding does not get broken.

In addition we should only be changing the value and triggering the changed event IF the value has actually changed.  Apply these changes to the properties as per below:

  • GetWeather
  • Title
  • Temperature
  • Wind
  • Humidity
  • Sunrise
  • Sunset

In the GetWeather method, you also need to pass the user selected Country Code.  The ?. operator is the equivalent of checking if the CountryCode is null, to prevent Null exceptions.

public async void GetWeather()
{
  weather = await Core.GetWeather(ZipCode, SelectedCountry?.CountryCode);

  OnPropertyChanged(nameof(Title));
  OnPropertyChanged(nameof(Temperature));
  OnPropertyChanged(nameof(Wind));
  OnPropertyChanged(nameof(Humidity));
  OnPropertyChanged(nameof(Visibility));
  OnPropertyChanged(nameof(Sunrise));
  OnPropertyChanged(nameof(Sunset));
}

public string Title
{
  get { return weather.Title; }
  set 
  {
    if (weather.Title != value)
    {
       weather.Title = value;
       OnPropertyChanged(nameof(Title));
    }
  }
}

We can also improve our OnPropertyChanged method to be threadsafe AND protect against there not being any code listening for our event, which can cause an exception:

protected virtual void OnPropertyChanged(string propertyName)
{
  var changed = PropertyChanged;

  if (changed != null)
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

The ?. operator is the equivalent of an inline   != null  

Now we need to expose our list of Countries so that it can be displayed on screen.  Still in core.cs add some variable to hold the available Countries and the Country selected by the user:

[__strong__]using System.Collections.ObjectModel;

namespace WeatherApp
{
  public class Core: INotifyPropertyChanged
  {
    private Country _SelectedCountry;     
    public Country SelectedCountry     
    { get { return _SelectedCountry; }       
      set       
      {          
        if (_SelectedCountry != value)          
        {             
          _SelectedCountry = value;             
          OnPropertyChanged(nameof(SelectedCountry));          
        }       
      }     
    }        
 
    public ObservableCollection<Country> Countries = Country.Init();

Unfortunately the current version of the Xamarin Picker does not seem to work when the DataContext is specified in the XAML.  So for now comment it out (in MainPage.xaml) and add a Picker to show a list of Countries:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"

             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

             xmlns:local="clr-namespace:WeatherApp"

             xmlns:vm="clr-namespace:WeatherApp"

             x:Class="WeatherApp.MainPage">

    <!--<ContentPage.BindingContext>
        <vm:Core/>
    </ContentPage.BindingContext>-->
   
    <StackLayout>
        <StackLayout Margin="10,0,0,0" VerticalOptions="Start" HorizontalOptions="Start" WidthRequest="400" BackgroundColor="#545454">
            <Label Text="Weather App" x:Name="lblTitle"/>

            <StackLayout HorizontalOptions="Start" Margin="10,10,0,0" VerticalOptions="Start"  WidthRequest="400">
                <Label Text="Search by Zip Code" FontAttributes="Bold" TextColor="White" Margin="10" x:Name="lblSearchCriteria" VerticalOptions="Start"/>
                <Picker x:Name="cbxCountry" WidthRequest="200" SelectedItem="{Binding SelectedCountry, Mode=TwoWay}" SelectedIndex="0" Title="Country" TextColor="White"/>
                <Label Text="Zip Code" TextColor="White" Margin="10" x:Name="lblZipCode"/>
                <StackLayout  Orientation="Horizontal" VerticalOptions="Start">
                    <Entry WidthRequest="100" x:Name="edtZipCode"  VerticalOptions="Start" Text="{Binding ZipCode, Mode=TwoWay}"/>
                    <Button Text="Get Weather" x:Name="btnGetWeather"  VerticalOptions="Start" Clicked="BtnGetWeather_Click"/>
                </StackLayout>
            </StackLayout>
        </StackLayout>
Now in the MainPage.xaml.cs  we need to create our Binding Context and link the Picker up to our list of Countries:
 
public MainPage()
{
  InitializeComponent();

  BindingContext = new Core();
   
  cbxCountry.ItemsSource = ((Core)this.BindingContext).Countries;
}

The ItemSource *should* be able to be set in the XAML, but again it does not seem to currently work in the current version of the Xamarin Picker.

If you run up the application now, you should have an additional drop-down list.   If you choose United States and put in our test zipcode of 80801, the program should work as in the previous two walkthrus.

However now you can choose a different Country, for example select Australia (AU) and enter the zipcode 4034:

Defaulting and Usability

I could have had the list of Countries just showing the Name, however I've added the Country Code.  When supporting your application, having an identifier can be invaluable.  Just don't make your users use / remember Ids and Unique codes - that is very unfriendly.

The App could be more user friendly.  Currently we default to the first Country in the list.  But in reality, users will *generally* be searching for the weather in the country they are currently in.  It is looking for these types of usability features that make people love your app.  

In the Core.cs add a new procedure:

public void SelectCountryByISOCode()
{
  foreach (Country item in Countries)
    if (item.CountryCode == System.Globalization.RegionInfo.CurrentRegion.TwoLetterISORegionName)
      SelectedCountry = item;
}

and in the MainPage.xaml.cs, add a call our new proc:

public MainPage()
{
  InitializeComponent();

  BindingContext = new Core();
   
  cbxCountry.ItemsSource = ((Core)this.BindingContext).Countries;

  ((Core)this.BindingContext).SelectCountryByISOCode();
}

Now if you run up the application again, your Country should be selected by default.

Again for better usability, we should ensure that the user starts in the 'best' place to minimise the amount of navigation:

protected override void OnAppearing()
{
  base.OnAppearing();

  if (cbxCountry.SelectedIndex > -1)
    edtZipCode.Focus();
  else
    cbxCountry.Focus();
}

and IF the user happens to be on a platform that accepts enter, trigger the button push:

public MainPage()
{
  InitializeComponent();

  BindingContext = new Core();

  cbxCountry.ItemsSource = ((Core)this.BindingContext).Countries;

  ((Core)this.BindingContext).SelectCountryByISOCode();

  edtZipCode.Completed += (s, e) => btnGetWeather_Click(s,e);
}

Start to remove Hard Coding

Windows can give us information about our selected region.  Of interest to us in our app is the Temperature units, Fahrenheit or Celsius.

In Core.cs, add a new variable:

private bool UseMetric;

and then update the Country property to populate this information:

public class Core: INotifyPropertyChanged
{
  private Country _SelectedCountry;
  public Country SelectedCountry
  { get { return _SelectedCountry; }
    set
    {
       if (_SelectedCountry != value)
       {
          _SelectedCountry = value;
          OnPropertyChanged(nameof(SelectedCountry));

         UseMetric = new RegionInfo(SelectedCountry?.CountryCode).IsMetric;

         ZipCode = "";
      }
    }
}

private string _ZipCode;
public string ZipCode
{
  get { return _ZipCode; }
  set
  { 
    if (_ZipCode != value)
    { 
      _ZipCode = value;
      OnPropertyChanged(nameof(ZipCode));
    } 
  }
}

We also clear out the Zipcode when changing country as the formats vary from country to country.

Add access to the Globalization features in Core.cs:

using System.Globalization;

 

In the GetWeather method, send this flag to the method that gets the Weather:

public async void GetWeather()
{
  weather = await Core.GetWeather(ZipCode, SelectedCountry?.CountryCode, UseMetric);

  OnPropertyChanged(nameof(Title));
  OnPropertyChanged(nameof(Temperature));

In the GetWeather async method, add the parameter, add a new variable to hold if the Country uses Metric or Imperial measurements and then pass that parameter in the queryString to the website:

public static async Task<Weather> GetWeather(string pZipCode, string pCountryCode, bool pUseMetric)
{
  //Sign up for a free API key at http://openweathermap.org/appid 
  string key = "f3748390cfea7374d3fb0580af0cf4ae";
  string queryString = default(String);

  //IF pUseMetric, set the unitsString to "metric" else "imperial"
  string unitsString = (pUseMetric ? "metric" : "imperial");

  if (String.IsNullOrEmpty(pCountryCode)) //If Country is not specified, Open Weather Defaults to the US.
    queryString = $"http://api.openweathermap.org/data/2.5/weather?zip={pZipCode},us&appid={key}&units={unitsString}";
  else
    queryString = $"http://api.openweathermap.org/data/2.5/weather?zip={pZipCode},{pCountryCode}&appid={key}&units={unitsString}";

 

Finally, we need to update the population of the results to use the appropriate units:

[__strong__]string temperatureUnits = (pUseMetric ? "C" : "F");
string windUnits = (pUseMetric ? "km/h" : "mph");

  dynamic results = await DataService.getDataFromService(queryString).ConfigureAwait(false);

  if (results["weather"] != null)
  {
    DateTime time = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);

    return new Weather()
    {
      Title = (string)results["name"],
      Temperature = $"{(string)results["main"]["temp"]} {temperatureUnits}",
      Wind = $"{(string)results["wind"]["speed"]} {windUnits}",
      Humidity = (string)results["main"]["humidity"] + " %",
      Visibility = (string)results["weather"][0]["main"],
      Sunrise = $"{time.AddSeconds((double)results["sys"]["sunrise"]).ToString()} UTC",
      Sunset = $"{time.AddSeconds((double)results["sys"]["sunset"]).ToString()} UTC"
    };
 }

Now run again for Austrlia and United States and you will see the correct units being displayed.

Clean Up Implementation

For those who have looked at MVVM, it would be clear that this Program is utilizing this model.  The Visual Components are in the View, Weather class is our model and Core.cs is our View Model.  However some of the aspects of our existing Program are not consistent with MVVM.  Next we will move our Program more into the MVVM model by moving more of the UI logic into the ViewModel.

First, let us remove the hard coded text within the button.  By binding it to our VM we can easily change the appearing text based on Context.

For example, maybe you have a Modify button that brings up a seperate page to display the full details of some data your user edits.  We would want the button text to say Modify.  But what if that data could be frozen?  In this scenario you would not be letting the user into the details page to modify, but only to View.  To convey information to the user without having to explain why fields are read-only, the VM (based on the data's state) changes the button text to View.  
 

In Core.cs add a new property for the button text:

public string ButtonContent { get { return "Get Weather"; } }

And in our MainPage.xaml update Text to be bound to our new property:

<StackLayout  Orientation="Horizontal" VerticalOptions="Start">
  <Entry WidthRequest="100" x:Name="edtZipCode"  VerticalOptions="Start" Text="{Binding ZipCode, Mode=TwoWay}"/>
  <Button Text="{Binding ButtonContent}" x:Name="btnGetWeather" VerticalOptions="Start" Clicked="BtnWeather_Click"/>
</StackLayout>

Another reason you may want to Bind your button caption is for support of multiple languages.

 

The next step is to bind the Click to the VM rather than C# code within our MainPage.xaml.  To do this we can implement a cut down version of ICommand in Core.cs:

using System.Windows.Input;

 

private bool _CanGetWeather = true;
public bool CanGetWeather
{
  get { return _CanGetWeather; }
  set
  {
    if (_CanGetWeather != value)
    {
      _CanGetWeather = value;
      OnPropertyChanged(nameof(CanGetWeather));
    }
  }
}

public ICommand GetWeather { get; private set; }

public Core()
{
  GetWeather = new Command(async () => await _GetWeather(), () => CanGetWeather);
}

Now we have the new Command, update our Button to call it:

<StackLayout  Orientation="Horizontal" VerticalOptions="Start">
  <Entry WidthRequest="100" x:Name="edtZipCode"  VerticalOptions="Start" Text="{Binding ZipCode, Mode=TwoWay}"/>
  <Button Text="{Binding ButtonContent}" x:Name="btnGetWeather" VerticalOptions="Start" IsEnabled="{Binding CanGetWeather, Mode=TwoWay}" Command="{Binding GetWeather}"/>
</StackLayout>

There is robust discussion as to if the Data Access Layer should be seperate, in the ViewModel or in the Model.  For the sake of this exercise, I've decided to put it within the model - by reason that the model is meant to understand its data, and to my mind that includes how to retrieve it.  I'm sure others will have different views, with very good points to be made.

Move the public async Task<Weather> GetWeather from Core.cs into Weather.cs.  I've also renamed the Task for clarity.  Finally, we need to change the scope to public so that it can be accessed:

[__strong__]public static async Task<Weather> RetrieveWeather(string pZipCode, string pCountryCode, bool pUseMetric)
{
  //Sign up for a free API key at http://openweathermap.org/appid 
  string key = "YOUR KEY";
  string queryString = default(String);

  string unitsString = (pUseMetric ? "metric" : "imperial");

  if (String.IsNullOrEmpty(pCountryCode)) //If Country is not specified, Open Weather Defaults to the US.
    queryString = $"http://api.openweathermap.org/data/2.5/weather?zip={pZipCode},us&appid={key}&units={unitsString}";
  else
    queryString = $"http://api.openweathermap.org/data/2.5/weather?zip={pZipCode},{pCountryCode}&appid={key}&units={unitsString}";

  //Make sure developers running this sample replaced the API key
  if (key != "YOUR KEY")
  {
    throw new ArgumentOutOfRangeException("You must obtain an API key from openweathermap.org/appid and save it in the 'key' variable.");
    return null;
  }

  string temperatureUnits = (pUseMetric ? "C" : "F");
  string windUnits = (pUseMetric ? "km/h" : "mph");

  try
  {
    dynamic results = await DataService.getDataFromService(queryString).ConfigureAwait(false);

    if (results["weather"] != null)
    {
      DateTime time = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);

      return new Weather()
      {
        Title = (string)results["name"],
        Temperature = $"{(string)results["main"]["temp"]} {temperatureUnits}",
        Wind = $"{(string)results["wind"]["speed"]} {windUnits}",
        Humidity = (string)results["main"]["humidity"] + " %",
        Visibility = (string)results["weather"][0]["main"],
        Sunrise = $"{time.AddSeconds((double)results["sys"]["sunrise"]).ToString()} UTC",
        Sunset = $"{time.AddSeconds((double)results["sys"]["sunset"]).ToString()} UTC"
      };
    }
    else
      throw new Exception((string)results["message"]);
  }
  catch (Exception ex)
  {
    //This catching and re-raising of an exception does not do much, however in larger applications
    //generally there is some form of error logging for reporting back to Administrators. That logging
    //would go here.  You could also have mulitple Catch statments to trap different types of errors
    //to give better feed-back to users as to what went wrong.
    throw new Exception(ex.Message);
  }
}

Re-name and re-write our existing Core.cs GetWeather method to be an async task _GetWeather, update our ZipCode Property and Weather variable:

private Weather _weather = new Weather();
public Weather Weather
{ get { return _weather; }
  private set
  {
    if ((_weather != value) && (value != null))
    {
      _weather = value;

      OnPropertyChanged(nameof(Title));
      OnPropertyChanged(nameof(Temperature));
      OnPropertyChanged(nameof(Wind));
      OnPropertyChanged(nameof(Humidity));
      OnPropertyChanged(nameof(Visibility));
      OnPropertyChanged(nameof(Sunrise));
      OnPropertyChanged(nameof(Sunset));
    }
  }
}

private async Task _GetWeather()
{
  CanGetWeather = false;

  try
  {
    Weather = await Weather.RetrieveWeather(ZipCode, SelectedCountry?.CountryCode, UseMetric);
  }
  finally
  {
    CanGetWeather = true;
  }
}

private string _ZipCode;
    public string ZipCode
    {
      get
      { return _ZipCode; }
      set
      { if (_ZipCode != value)
        {
          _ZipCode = value;
          OnPropertyChanged(nameof(ZipCode));

          CanGetWeather = !String.IsNullOrEmpty(value);
        }
      }
    }

Still in core.cs add using:

using Xamarin.Forms;

Finally, we update our MainPage.cs & MainPage.xaml to use our updated ViewModel:

public MainPage()
{
  InitializeComponent();

  BindingContext = new Core();

  cbxCountry.ItemsSource = ((Core)this.BindingContext).Countries;

  ((Core)this.BindingContext).SelectCountryByISOCode();

  edtZipCode.Completed += (s, e) => this.GetWeather();
}

private void GetWeather()
{
  if (((Core)BindingContext).GetWeather.CanExecute(null))
    ((Core)BindingContext).GetWeather.Execute(null);
}

Using Converters to remove Hard Coding

In general we would not store our returned data as strings, but in more appropriate data types, for example Wind, Humidity & Temperature Doubles.  To then display the data nicely we have several options, unfortunately a number of these options are not available in Xamarin...

In general WPF we would use a Converter and/or multi-binding to show the information.  Multi-Binding is not yet implemented in Xamarin and we cannot use a Converter as you cannot Bind the ConverterParameter to the IsMetric value.  That said, there are a number of extensions available on the internet that do allow you to Bind values to ConverterParameters, which is how I would ideally resolve this problem.

For the sake of this exercise however, I do not want to rely on extensions and this also illustrates that there are often several ways to resolve a problem.

Having a Display Property

This is my least preferred option, as this is one of the reasons Converters exist.  It works in this scenario, however if the control being bound to is a Textbox/Entry control - we would have to parse the returned value.

In weather.cs update the Temperature property to be a double.

public class Weather
{
  public string Title { get; set; }
  public double Temperature { get; set; }
  public string Wind { get; set; }
  public string Humidity { get; set; }
  public string Visibility { get; set; }
  public string Sunrise { get; set; }
  public string Sunset { get; set; }

  private bool _useMetric;
   

  public Weather()
  {
    //Because labels bind to these values, set them to an empty string to 
    //ensure that the label appears on all platforms by default. 
    this.Title = default(string);
    this.Temperature = default(double);
    this.Wind = default(string);
    this.Humidity = default(string);
    this.Visibility = default(string);
    this.Sunrise = default(string);
    this.Sunset = default(string);
  }

In core.cs update the 

public double Temperature
{
  get { return _weather.Temperature; }
  set
  {
    _weather.Temperature = value;
   
    OnPropertyChanged(nameof(Temperature));
    OnPropertyChanged(nameof(TemperatureDisplay));
  }
}
public string TemperatureDisplay => $"{Temperature.ToString()} {(UseMetric ? TemperatureUnits.tuCelsius.EnumDescription() : TemperatureUnits.tuFahrenheit.EnumDescription())}";

 

private Weather _weather = new Weather();
public Weather Weather
{
  get { return _weather; }
  private set
  {
    if (_weather != value)
    {
      _weather = value;

      OnPropertyChanged(nameof(Title));
      OnPropertyChanged(nameof(Temperature));
      OnPropertyChanged(nameof(TemperatureDisplay));
      OnPropertyChanged(nameof(Wind));

 

In MainPage.xaml we bind to the new value:

<StackLayout VerticalOptions="StartAndExpand">
  <Label Text ="Location" TextColor="#FFA8A8A8" FontSize="14"/>
  <Label Text ="{Binding Title}" Margin="10,0,0,10" x:Name="txtLocation"/>
  <Label Text ="Temperature" TextColor="#FFA8A8A8" FontSize="14"/>
  <Label Text ="{Binding TemperatureDisplay}" Margin="10,0,0,10" x:Name="txtTemperature"/>

In Weather.cs create an enumeration.  Remember to delete the TemperatureUnits string variable from Weather.cs, RetrieveWeather method.

[__strong__]using System.ComponentModel;
using System.Threading.Tasks;

namespace WeatherApp
{
  public enum TemperatureUnits
  {
    [Description("C")]
    tuCelsius,
    [Description("F")]
    tuFahrenheit
  }

  public class Weather

We also need to include soem code to be able to use our Descriptions, since this is not currently available in Xamarin.  In the Solution Explorer, right click on the WeatherApp Project, Add, Class and name it DescriptionAttribute.cs:

using System;
using System.Linq;
using System.Reflection;

//http://taoffi.isosoft.org/post/2017/05/03/Xamarin-missing-Description-attribute

namespace WeatherApp
{
  [AttributeUsage(validOn: AttributeTargets.All, AllowMultiple = false, Inherited = false)]
  public class DescriptionAttribute : Attribute
  {
    public string _description;

    public DescriptionAttribute(string description)
    {
      _description = description;
    }

    public string Description
    {
      get { return _description; }
      set { _description = value; }
    }
  }

  public static class EnumDescriptions
  {
    public static string EnumDescription(this Enum value)
    {
      if (value == null)
        return null;

      var type = value.GetType();
      TypeInfo typeInfo = type.GetTypeInfo();
      string typeName = Enum.GetName(type, value);

      if (string.IsNullOrEmpty(typeName))
        return null;

      var field = typeInfo.DeclaredFields.FirstOrDefault(f => f.Name == typeName);

      if (field == null)
        return typeName;

      var attrib = field.GetCustomAttribute<DescriptionAttribute>();
      return attrib == null ? typeName : attrib.Description;
    }
  }
}

Finally, we have to update RetrieveWeather in Weather.cs to populate the Temp as a number, not a string:

      if (results["weather"] != null)
      {
        DateTime time = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);

        return new Weather()
        {
          Title = (string)results["name"],
          //Temperature = $"{(string)results["main"]["temp"]} {Temperature}",
          Temperature = (double)results["main"]["temp"],
          Wind = $"{(string)results["wind"]["speed"]} {windUnits}",

 

Now when you run up the application, you see the same details.  This methodology has a side effect of dispalying the value of 0 F (instead of being blank).   

Multiple fields

As we cannot have multi-binding, we can use multiple fields.

In MainPage.xaml plase the exisitng Wind label with two tables contained within a StackLayout:

<Label Text ="Wind Speed" TextColor="#FFA8A8A8" FontSize="14"/>
<StackLayout Orientation="Horizontal">
  <Label Text ="{Binding Wind}" Margin="10,0,0,10" x:Name="txtWind"/>
  <Label Text ="{Binding WindUnits}" Margin="10,0,0,10" x:Name="txtWindUnits"/>
</StackLayout>
<Label Text ="Humidity" TextColor="#FFA8A8A8" FontSize="14"/>

In core.cs update our Wind Property to be a double and add a New Property to return the Wind Units

public double Wind
{
  get { return _weather.Wind; }
  set
  {
    _weather.Wind = value;
    OnPropertyChanged(nameof(Wind));
  }
}
public string WindUnits => $"{(UseMetric ? WindUnits.wuKmh.EnumDescription() : WindUnits.wuMph.EnumDescription())}";

Also update the Wind initialisation to be a double and have an OnPropertyChanged to update the screen:

public Weather Weather
    { get { return _weather; }
      private set
      {
        if (_weather != value)
        {
          _weather = value;

          OnPropertyChanged(nameof(Title));
          OnPropertyChanged(nameof(Temperature));
          OnPropertyChanged(nameof(TemperatureDisplay));
          OnPropertyChanged(nameof(Wind));
          OnPropertyChanged(nameof(WindUnits));
          OnPropertyChanged(nameof(Humidity));
          OnPropertyChanged(nameof(Visibility));
          OnPropertyChanged(nameof(Sunrise));
          OnPropertyChanged(nameof(Sunset));
        }
      }
    }

In Weather.cs add a new enum for the units and update the Wind property to be a double:

namespace WeatherApp
{
  public enum TemperatureUnits
  {
    [Description("C")]
    tuCelsius,
    [Description("F")]
    tuFahrenheit
  }
  public enum WindUnits
  {
    [Description("mph")]
    wuMph,
    [Description("km/h")]
    wuKmh
  }

public class Weather
  {
    public string Title { get; set; }
    public double Temperature { get; set; }
    public double Wind { get; set; }
    public double Humidity { get; set; }

Delete the WindUnits string from Weather.cs.

public Weather()
{
  //Because labels bind to these values, set them to an empty string to 
  //ensure that the label appears on all platforms by default. 
  this.Title = default(string);
  this.Temperature = default(double);
  this.Wind = default(double);
  this.Humidity = default(double);
  this.Visibility = default(string);
  this.Sunrise = default(string);
  this.Sunset = default(string);
}

Finally in RetrieveWeather task update where the values are populated:

try
{
  dynamic results = await DataService.getDataFromService(queryString).ConfigureAwait(false);

  if (results["weather"] != null)
  {
    DateTime time = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);
         
    return new Weather()
          {
            Title = (string)results["name"],
            //Temperature = $"{(string)results["main"]["temp"]} {temperatureUnits}",
            _useMetric = pUseMetric,
            Temperature = (double)results["main"]["temp"],
            //Wind = $"{(double)results["wind"]["speed"]} {windUnits}",
            Wind = (double)results["wind"]["speed"],
            //Humidity = (double)results["main"]["humidity"] + " %",
            Humidity = (double)results["main"]["humidity"],
            Visibility = (string)results["weather"][0]["main"],
            Sunrise = $"{time.AddSeconds((double)results["sys"]["sunrise"]).ToString()} UTC",
            Sunset = $"{time.AddSeconds((double)results["sys"]["sunset"]).ToString()} UTC"
          };
        }

To ensure our Humidty value still displays the % update MainPage.xaml with a Converter:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"

             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

             xmlns:local="clr-namespace:WeatherApp"

             xmlns:vm="clr-namespace:WeatherApp"

             x:Class="WeatherApp.MainPage">

<ContentPage.Resources>
  <ResourceDictionary>
    <local:PercentageConverter x:Key="PercentageCustomString"/>
  </ResourceDictionary>
</ContentPage.Resources>
<Label Text ="Humidity" TextColor="#FFA8A8A8" FontSize="14"/>
<Label Text ="{Binding Humidity, Converter={StaticResource PercentageCustomString}, Mode=OneWay}" Margin="10,0,0,10" x:Name="txtHumidity"/>
<Label Text ="Visibility" TextColor="#FFA8A8A8" FontSize="14"/>

In Core.cs:

public double Humidity
{
  get { return _weather.Humidity; }
  set
  {
    if (_weather.Humidity != value)
    {
      _weather.Humidity = value;
      OnPropertyChanged(nameof(Humidity));
    }
  }
}

In Weather.cs

if (results["weather"] != null)
{
  DateTime time = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);

  return new Weather()
  {
    Title = (string)results["name"],
    //Temperature = $"{(string)results["main"]["temp"]} {Temperature}",
    Temperature = (double)results["main"]["temp"],
    //Wind = $"{(double)results["wind"]["speed"]} {windUnits}",
    Wind = (double)results["wind"]["speed"],
    Humidity = (double)results["main"]["humidity"],
    Visibility = (string)results["weather"][0]["main"],

Add a New Class to the project named Percentage.cs:

using System;
using System.Globalization;
using Xamarin.Forms;

namespace WeatherApp
{
  class PercentageConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
      return $"{value.ToString()} %";
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
      throw new NotImplementedException();
    }
  }
}

I find, I often have to Close our of Visual Studio to ensure a Converter registers correctly.

Run our application again and the Wind, Wind Units and Humidity are printed up nicely.

Feedback to Users / Error Messages

For the finaly part of this walkthru we need to provide some feeback to users.  In our application the most frequent problems to catch are:

  1. Invalid Country / ZipCode
  2. Lack of Internet connection

In MainPage.xaml add a new control to display any error state:

<StackLayout Margin="10,0,0,0" VerticalOptions="Start" HorizontalOptions="Start" WidthRequest="400" BackgroundColor="#545454">
  <Label Text="Weather App" x:Name="lblTitle"/>

  <StackLayout HorizontalOptions="Start" Margin="10,10,0,0" VerticalOptions="Start"  WidthRequest="400">
    <Label Text="Search by Zip Code" FontAttributes="Bold" TextColor="White" Margin="10" x:Name="lblSearchCriteria" VerticalOptions="Start"/>
    <Picker x:Name="cbxCountry" WidthRequest="200" SelectedItem="{Binding SelectedCountry, Mode=TwoWay}" SelectedIndex="0" Title="Country"  TextColor="White"/>
    <Label Text="Zip Code" TextColor="White" Margin="10" x:Name="lblZipCode"/>
    <StackLayout  Orientation="Horizontal" VerticalOptions="Start">
      <Entry WidthRequest="100" x:Name="edtZipCode"  VerticalOptions="Start" Text="{Binding ZipCode, Mode=TwoWay}"/>
      <Button Text="{Binding ButtonContent}" x:Name="btnGetWeather" VerticalOptions="Start" IsEnabled="{Binding CanGetWeather, Mode=TwoWay}" Command="{Binding GetWeather}"/>
    </StackLayout>
    <Label Text="{Binding ErrorMessage}" Margin="10" x:Name="lblErrorMessage" TextColor="DarkRed"/>
  </StackLayout>
</StackLayout>

 

In Core.cs add a new variable and property to store an Error State and create a new try..catch to populate:

[__strong__]private string _ErrorMessage = default(string);
public string ErrorMessage
{
  get { return _ErrorMessage; }
  set
  {
    if (value != _ErrorMessage)
    {
      _ErrorMessage = value;
      OnPropertyChanged(nameof(ErrorMessage));
    }
  }
}

private async Task _GetWeather()
{
  CanGetWeather = false;

  try
  {
    try
    {
      ErrorMessage = default(string);

      //Clear our the results so that we are not displaying incorrect information should an excption occur.
      Title = default(String);
      Temperature = default(double);
      Wind = default(double);
      Humidity = default(double);
      Visibility = default(String);
      Sunrise = default(String);
      Sunset = default(String);

      Weather = await Weather.RetrieveWeather(ZipCode, SelectedCountry?.CountryCode, UseMetric);
    }
    catch (Exception ex)
    {
      ErrorMessage = ex.Message;
    }
  }
  finally
  {
    CanGetWeather = true;
  }
}

When you now run, enter an incorrect Zip code we get a nice error message.   However if you disconnection your PC and run, we get an error message.  But it is not very user friendly.

Update Weather.cs with some more helpful error message information when there is no data returned:

   throw new ArgumentOutOfRangeException("You must obtain an API key from openweathermap.org/appid and save it in the 'key' variable.");

 dynamic results = null;

  try
  {
    results = await DataService.getDataFromService(queryString).ConfigureAwait(false);

    if (results["weather"] != null)
    {
      DateTime time = new System.DateTime(1970, 1, 1, 0, 0, 0, 0);
         
      return new Weather()
          {
            Title = (string)results["name"],
            _useMetric = pUseMetric,
            Temperature = (double)results["main"]["temp"],
            Wind = (double)results["wind"]["speed"],
            Humidity = (double)results["main"]["humidity"],
            Visibility = (string)results["weather"][0]["main"],
            Sunrise = $"{time.AddSeconds((double)results["sys"]["sunrise"]).ToString()} UTC",
            Sunset = $"{time.AddSeconds((double)results["sys"]["sunset"]).ToString()} UTC"
         };
     }
     else
        throw new Exception((string)results["message"]);
    }
    catch (Exception ex)
    {
      if (results == null)
        throw new Exception($"Unable to retreived Weather data.  Please check your internet connection ({ex.Message})");
      else
        throw new Exception(ex.Message);
    }
  }

 

Where to from here?

This is the last in this series - thank you for reading!  There are so many ways you can continue to extend this applcation.   If I were going to continue, I would extend this application by...

  1. Hiding the results labels until they are populated.
  2. Creating a new Converter for Visibility that returns a picture and displaying that as a background (e.g. sunny, overcast, rain).
  3. Changing the times to be in Local time (i.e. the place the weather is) an/or time at your devices location.
  4. Putting the Country details into an SQLite database.
  5. Adding a Configuration screen so that the user can select colours, if they want temp/wind to be displayed in the regions preference, or always in C/kwh or F/mph (or both).
  6. Be more creative with the controls to better handle re-sizing.
  7. There are also a number of additional features available on the weather website that could be integrated, like historic weather conditions.  Show what the weather was last year compared to today.

For those who want a copy of the source code (feel free to branch and make your own improvements), it is located on GitHub:   https://github.com/Rodimus7/WeatherApp.git

 

I look forward to seeing your comments as to how you have taken this example and made your own application.

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Rodimus74
Web Developer
Australia Australia
No Biography provided

You may also be interested in...

Comments and Discussions

 
Questionnot well formatted and few images are missing Pin
Mou_kol12-Sep-17 22:46
memberMou_kol12-Sep-17 22:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.170915.1 | Last Updated 13 Sep 2017
Article Copyright 2017 by Rodimus74
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid