Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Google Weather API on Windows Phone 7

DZone's Guide to

Google Weather API on Windows Phone 7

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

I am not entirely sure how good the default weather application is – the one bundled with the Windows Phone 7 system, but I’ve always wanted to develop my own app that would display the weather my way. For this purpose, I’ve experimented quite a while with Google Weather API. Although not documented, it’s an API that is easy-to-use and quite informative at the same time.

Every API call is made through a simple HTTP request that returns XML-formatted data. Initially, I thought about developing a custom control that would show the conditions for a specific location, but then I decided to simply list them and pass a custom DataTemplate for each forecasted day.

Generally, I tried to keep the UI very simple – there is a TextBlock that will show the current location, a TextBox, that will be the place where the user will enter the location, a Button that will trigger the update and a ListBox that will contain the forecast for the next four days.

So to speak, the XAML for the main page looks like this:

<phone:PhoneApplicationPage
    x:Class="WeatherAlerts.MainPage" x:Name="HeadPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
    shell:SystemTray.IsVisible="True" Loaded="PhoneApplicationPage_Loaded">

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="160"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBlock  Margin="10,-15,0,0" Grid.Row="0" x:Name="PageTitle" Style="{StaticResource PhoneTextTitle1Style}"/>
        <TextBox Grid.Row="0" Margin="0,90,100,0" Name="locationBox"></TextBox>
        <Button Margin="360,90,0,0" Content="GET" Click="Button_Click"></Button>

        <ListBox Grid.Row="1" x:Name="weatherList" ItemsSource="{Binding ElementName=HeadPage,Path=Conditions}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid Height="100">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="100"></ColumnDefinition>
                            <ColumnDefinition Width="*"></ColumnDefinition>
                        </Grid.ColumnDefinitions>

                        <Image Grid.Column="0" Height="100" Width="100" Source="{Binding Path=Icon}"></Image>
                        <TextBlock Text="{Binding Path=Day}" Grid.Column="1" Margin="10,10,10,60"></TextBlock>

                        <TextBlock Grid.Column="1" Margin="10,40,310,30" Text="LOW:"></TextBlock>
                        <TextBlock Text="{Binding Path=Low}" Grid.Column="1" Margin="70,40,250,30" FontWeight="Bold"></TextBlock>

                        <TextBlock Grid.Column="1" Margin="150,40,170,30" Text="HIGH:"></TextBlock>
                        <TextBlock Text="{Binding Path=High}" Grid.Column="1" Margin="215,40,90,30" FontWeight="Bold"></TextBlock>

                        <TextBlock Text="{Binding Path=Condition}" Grid.Column="1" Margin="10,75,10,0"></TextBlock>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</phone:PhoneApplicationPage>

As you can see, the ListBox control is bound to a collection. I chose an ObservableCollection in order to make the binding easier – I won’t have to manually re-bind the collection to the control. The actual collection is also represented by a DependencyProperty in the main class:

public static readonly DependencyProperty _conditions = DependencyProperty.Register("Conditions", typeof(ObservableCollection<WeatherElement>), typeof(PhoneApplicationPage), new PropertyMetadata(null));
public ObservableCollection<WeatherElement> Conditions
{
    get { return (ObservableCollection<WeatherElement>)GetValue(_conditions); }
    set { SetValue(_conditions, value); }
}

One unknown class here is WeatherElement – it is used to display the details for each forecasted day. The class is composed out of five properties, that are set on instantiation:

public class WeatherElement
{
    public BitmapImage Icon {get;set;}
    public string Day { get; set; }
    public string Low { get; set; }
    public string High { get; set; }
    public string Condition { get; set; }
}

The Icon property will also be set based on the information from Google Weather API – each condition has an image associated with it. There is one problem with the image, though – it is presented in GIF file format – and it is not supported in Silverlight. Of course, there is a workaround for this and I will discuss it later on in this article.

But now, let’s look at the main method that will retrieve data needed to fill the ObservableCollection:

void GetData()
{
    string location = IsolatedStorageSettings.ApplicationSettings["location"].ToString();
    var sc = SynchronizationContext.Current;

    if (!string.IsNullOrEmpty(location))
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://www.google.com/ig/api?weather=" + location);
        request.BeginGetResponse(asyncResult =>
            {
                HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);

                XDocument doc = XDocument.Load(response.GetResponseStream());
                if (doc.Root.Element("problem_cause") == null)
                {
                    var x = from c in doc.Root.Element("weather").Elements() where c.Name == "forecast_conditions" select c;

                    foreach (XElement element in x)
                    {
                        image = null;
                        WeatherElement welement = new WeatherElement();
                        welement.Condition = element.Element("condition").Attribute("data").Value;
                        welement.Day = element.Element("day_of_week").Attribute("data").Value;
                        welement.High = element.Element("high").Attribute("data").Value;
                        welement.Low = element.Element("low").Attribute("data").Value;

                        WebClient client = new WebClient();
                        client.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);
                        client.OpenReadAsync(new Uri("http://www.google.com/"+ element.Element("icon").Attribute("data").Value));

                        while (image == null)
                        {
                            Thread.Sleep(0);
                        }

                        welement.Icon = image;

                        sc.Post(postData =>
                        {
                            Conditions.Add(welement);
                        }, null);
                    }
                }
            }
            ,null);
        
    }
}

As you see, I am sending a regular web request that is processed asynchronously. Then, I am obtaining the current conditions with the help of LINQ to select only the forecasted conditions (since there is another data set present in the returned XML as well).

You probably noticed that before adding the WeatherElement to the collection, I am creating a new web request to receive the image. The only problem, as I already mentioned, is the fact that the image is in GIF format.

I created a WCF service (see this article on how to do it) that will convert the GIF to a JPEG. The OpenReadAsync triggers the following event handler when completed:

void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    byte[] imageContents;

    using (BinaryReader reader = new BinaryReader(e.Result))
    {
        imageContents = reader.ReadBytes((int)e.Result.Length);
    }

    ServiceReference1.ConverterClient client = new ServiceReference1.ConverterClient();
    client.ConvertGifToJpegCompleted += new EventHandler<ServiceReference1.ConvertGifToJpegCompletedEventArgs>(client_ConvertGifToJpegCompleted);
    client.ConvertGifToJpegAsync(imageContents);
}

Here I am referencing the WCF service I created (a service reference was added prior) and I am calling yet another asynchronous action that will convert the image (the method is registered as a part of the service):

void client_ConvertGifToJpegCompleted(object sender, ServiceReference1.ConvertGifToJpegCompletedEventArgs e)
{
    using (MemoryStream stream = new MemoryStream(e.Result))
    {
        BitmapImage image = new BitmapImage();
        image.SetSource(stream);
        this.image = image;
    }
    
}

this.image is an instance of BitmapImage that is available for the entire class, declared in the class header. When the asynchronous operation is launched, the GetData method waits for this BitmapImage to get a value. Once it is different from null, it is added to the WeatherElement instance (the Icon property), and that is added to the collection that is bound to the ListBox.

Now to the easier part of the application - the actual method calls. When the user clicks the button, you want to reset the collection and set the application setting to the new value – a new location.

private void Button_Click(object sender, RoutedEventArgs e)
{
    if (!string.IsNullOrEmpty(locationBox.Text))
    {
        PageTitle.Text = IsolatedStorageSettings.ApplicationSettings["location"].ToString();
        IsolatedStorageSettings.ApplicationSettings["location"] = locationBox.Text;
        IsolatedStorageSettings.ApplicationSettings.Save();

        Conditions = new ObservableCollection<WeatherElement>();
        GetData();
    }
    else
    {
        MessageBox.Show("No location entered.", "LOCATION", MessageBoxButton.OK);
    }
}

This will set the page title to the new location and will also save the location in the application settings. It also resets the collection by re-instantiating it. Here, you might want to say that I could’ve bound the location property to the Text property for the TextBlock showing the title. However, I am only using this call in two places – when the user pressed the GET button and when the application loads:

private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
    if (IsolatedStorageSettings.ApplicationSettings["location"] != null)
    {
        PageTitle.Text = IsolatedStorageSettings.ApplicationSettings["location"].ToString();

        Conditions = new ObservableCollection<WeatherElement>();
        GetData();
    }
}

Therefore, I left it without direct binding. When you launch the application, you should get a result similar to this, given that the GIF conversion service is running and Google Weather is available.

In this article I didn't include the current conditions, that are available through the API. You can implement it by simply adding another LINQ query.

The reason I am adding an extra image conversion layer is to avoid bundling a whole new set of icons - that will take extra space and resources. Also, direct conversion in the application without invoking the actual service will result in major resource consumtion, especially on a device where hardware has very limited performance, therefore it is reasonable to delegate the conversion to an external resource.

For experimentation purposes, you can download the test project here.

The service (for conversion) source code can be downloaded here.

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}