Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF
Print

WPF: OpenWeather

4.98/5 (62 votes)
15 Sep 2017CPOL2 min read 170.2K   8.2K  
WPF-MVVM weather forecast application that displays weather data using the OpenWeatherMap API

Introduction

OpenWeather is a WPF-MVVM weather forecast application that displays three day (current day + two days following) forecast for a particular location. The app makes use of the OpenWeatherMap API.

To get the project code, clone or download the project from GitHub.

Background

I wrote the first version of this app back in 2013, when the OpenWeatherMap API was still in beta. After taking a look at the old code, I decided it was time for a rewrite. As part of the update, the UI has been redesigned and hopefully looks much better than before.

ye olde app UI

Requirements

To use this project effectively, you require the following:

  • OpenWeatherMap App ID
  • Internet connection
  • Visual Studio 2015/17

Once you get your App ID, insert it as the value of the APP_ID constant in the OpenWeatherMapService class.

private const string APP_ID = "PLACE-YOUR-APP-ID-HERE";
...

OpenWeatherMap

The OpenWeatherMap API provides weather data in XML, JSON, and HTML formats. The service is free for limited API calls – less than 60 calls per minute, and maximum 5 day forecast.

This project makes use of the XML response:

XML
<weatherdata>
  <location>
    <name>Nairobi</name>
    <type/>
    <country>KE</country>
    <timezone/>
    <location altitude="0" latitude="-1.2834" longitude="36.8167" 
     geobase="geonames" geobaseid="184745"/>
  </location>
  <credit/>
  <meta>
    <lastupdate/>
    <calctime>0.0083</calctime>
    <nextupdate/>
  </meta>
  <sun rise="2017-08-17T03:34:28" set="2017-08-17T15:38:48"/>
  <forecast>
    <time day="2017-08-17">
      <symbol number="501" name="moderate rain" var="10d"/>
      <precipitation value="5.03" type="rain"/>
      <windDirection deg="54" code="NE" name="NorthEast"/>
      <windSpeed mps="1.76" name="Light breeze"/>
      <temperature day="24.55" min="16.92" max="24.55" 
                   night="16.92" eve="24.55" morn="24.55"/>
      <pressure unit="hPa" value="832.78"/>
      <humidity value="95" unit="%"/>
      <clouds value="scattered clouds" all="48" unit="%"/>
    </time>
    <time day="2017-08-18">
      <symbol number="500" name="light rain" var="10d"/>
      <precipitation value="0.65" type="rain"/>
      <windDirection deg="109" code="ESE" name="East-southeast"/>
      <windSpeed mps="1.62" name="Light breeze"/>
      <temperature day="21.11" min="14.33" max="23.5" 
                   night="17.81" eve="21.48" morn="14.33"/>
      <pressure unit="hPa" value="836.23"/>
      <humidity value="62" unit="%"/>
      <clouds value="clear sky" all="8" unit="%"/>
    </time>
    <time day="2017-08-19">
      <symbol number="500" name="light rain" var="10d"/>
      <precipitation value="2.47" type="rain"/>
      <windDirection deg="118" code="ESE" name="East-southeast"/>
      <windSpeed mps="1.56" name=""/>
      <temperature day="22.55" min="12.34" max="22.55" 
                   night="15.46" eve="20.92" morn="12.34"/>
      <pressure unit="hPa" value="836.28"/>
      <humidity value="56" unit="%"/>
      <clouds value="clear sky" all="0" unit="%"/>
    </time>
  </forecast>
</weatherdata>

Fetching Forecast Data

Getting the forecast data is done by a function in the implementation of an interface named IWeatherService.

public class OpenWeatherMapService : IWeatherService
{
    private const string APP_ID = "PLACE-YOUR-APP-ID-HERE";
    private const int MAX_FORECAST_DAYS = 5;
    private HttpClient client;

    public OpenWeatherMapService()
    {
        client = new HttpClient();
        client.BaseAddress = new Uri("http://api.openweathermap.org/data/2.5/");
    }

    public async Task<IEnumerable<WeatherForecast>> 
           GetForecastAsync(string location, int days)
    {
        if (location == null) throw new ArgumentNullException("Location can't be null.");
        if (location == string.Empty) throw new ArgumentException
           ("Location can't be an empty string.");
        if (days <= 0) throw new ArgumentOutOfRangeException
           ("Days should be greater than zero.");
        if (days > MAX_FORECAST_DAYS) throw new ArgumentOutOfRangeException
           ($"Days can't be greater than {MAX_FORECAST_DAYS}");

        var query = $"forecast/daily?q={location}&type=accurate&mode=xml&units=metric&
                    cnt={days}&appid={APP_ID}";
        var response = await client.GetAsync(query);

        switch (response.StatusCode)
        {
            case HttpStatusCode.Unauthorized:
                throw new UnauthorizedApiAccessException("Invalid API key.");
            case HttpStatusCode.NotFound:
                throw new LocationNotFoundException("Location not found.");
            case HttpStatusCode.OK:
                var s = await response.Content.ReadAsStringAsync();
                var x = XElement.Load(new StringReader(s));

                var data = x.Descendants("time").Select(w => new WeatherForecast
                {
                    Description = w.Element("symbol").Attribute("name").Value,
                    ID = int.Parse(w.Element("symbol").Attribute("number").Value),
                    IconID = w.Element("symbol").Attribute("var").Value,
                    Date = DateTime.Parse(w.Attribute("day").Value),
                    WindType = w.Element("windSpeed").Attribute("name").Value,
                    WindSpeed = double.Parse(w.Element("windSpeed").Attribute("mps").Value),
                    WindDirection = w.Element("windDirection").Attribute("code").Value,
                    DayTemperature = double.Parse
                                     (w.Element("temperature").Attribute("day").Value),
                    NightTemperature = double.Parse
                                       (w.Element("temperature").Attribute("night").Value),
                    MaxTemperature = double.Parse
                                     (w.Element("temperature").Attribute("max").Value),
                    MinTemperature = double.Parse
                                     (w.Element("temperature").Attribute("min").Value),
                    Pressure = double.Parse(w.Element("pressure").Attribute("value").Value),
                    Humidity = double.Parse(w.Element("humidity").Attribute("value").Value)
                });

                return data;
            default:
                throw new NotImplementedException(response.StatusCode.ToString());
        }
    }
}

In GetForecastAsync(), I'm fetching the data asynchronously and using LINQ to XML to create a collection of WeatherForecast objects. (In the VB code, I'm using XML axis properties to access XElement objects and to extract values from the necessary attributes.) A command in WeatherViewModel will be used to call this function.

private ICommand _getWeatherCommand;
public ICommand GetWeatherCommand
{
    get
    {
        if (_getWeatherCommand == null) _getWeatherCommand =
                new RelayCommandAsync(() => GetWeather(), (o) => CanGetWeather());
        return _getWeatherCommand;
    }
}

public async Task GetWeather()
{
    try
    {
        var weather = await weatherService.GetForecastAsync(Location, 3);
        CurrentWeather = weather.First();
        Forecast = weather.Skip(1).Take(2).ToList();
    }
    catch (HttpRequestException ex)
    {
        dialogService.ShowNotification
        ("Ensure you're connected to the internet!", "Open Weather");
    }
    catch (LocationNotFoundException ex)
    {
        dialogService.ShowNotification("Location not found!", "Open Weather");
    }
}

The GetWeatherCommand is fired when the user types in a location and presses the Enter key.

XML
<TextBox x:Name="LocationTextBox" 
 SelectionBrush="{StaticResource PrimaryLightBrush}" Margin="7,0"
            VerticalAlignment="Top" 
            controls:TextBoxHelper.Watermark="Type location & press Enter"
            VerticalContentAlignment="Center"
            Text="{Binding Location, UpdateSourceTrigger=PropertyChanged}">
    <i:Interaction.Behaviors>
        <utils:SelectAllTextBehavior/>
    </i:Interaction.Behaviors>
    <TextBox.InputBindings>
        <KeyBinding Command="{Binding GetWeatherCommand}" Key="Enter"/>
    </TextBox.InputBindings>
</TextBox>

Displaying the Data

The current weather data is displayed in a user control named CurrentWeatherControl. The control displays a weather icon, temperature, weather description, maximum and minimum temperatures, and wind speed.

XML
<UserControl x:Class="OpenWeatherCS.Controls.CurrentWeatherControl"
               xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
               xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
               xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
               xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
               xmlns:local="clr-namespace:OpenWeatherCS.Controls"
               xmlns:data="clr-namespace:OpenWeatherCS.SampleData"
               mc:Ignorable="d"
               Height="180" MinHeight="180" MinWidth="300"
               d:DesignHeight="180" d:DesignWidth="300"
               d:DataContext="{d:DesignInstance Type=data:SampleWeatherViewModel, 
                               IsDesignTimeCreatable=True}">
    <Grid Background="{StaticResource PrimaryMidBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="106"/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <!-- Weather icon -->
            <Image Margin="5">
                <Image.Source>
                    <MultiBinding Converter="{StaticResource WeatherIconConverter}" 
                     Mode="OneWay">
                        <Binding Path="CurrentWeather.ID"/>
                        <Binding Path="CurrentWeather.IconID"/>
                    </MultiBinding>
                </Image.Source>
            </Image>
            <!-- Current temperature -->
            <TextBlock Grid.Column="1" 
             Style="{StaticResource WeatherTextStyle}" FontSize="60">
                <TextBlock.Text>
                    <MultiBinding Converter="{StaticResource TemperatureConverter}" 
                     StringFormat="{}{0:F0}°">
                        <Binding Path="CurrentWeather.IconID"/>
                        <Binding Path="CurrentWeather.DayTemperature"/>
                        <Binding Path="CurrentWeather.NightTemperature"/>
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </Grid>

        <!-- Weather description -->
        <Grid Grid.Row="1" Background="{StaticResource PrimaryDarkBrush}">
            <TextBlock Grid.Row="1" Style="{StaticResource WeatherTextStyle}"
                       Foreground="#FFE9F949" TextTrimming="CharacterEllipsis" Margin="5,0"
                       Text="{Binding CurrentWeather.Description}"/>
        </Grid>

        <Grid Grid.Row="2" Background="#FF14384F">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <!-- Min and max temperatures -->
            <Border Background="{StaticResource PrimaryLightBrush}" 
                    SnapsToDevicePixels="True">
                <TextBlock Grid.Row="1" Style="{StaticResource WeatherTextStyle}">
                    <Run Text="{Binding CurrentWeather.MaxTemperature, 
                         StringFormat={}{0:F0}°}"/>
                    <Run Text="/" Foreground="Gray"/>
                    <Run Text="{Binding CurrentWeather.MinTemperature, 
                         StringFormat={}{0:F0}°}"/>
                </TextBlock>
            </Border>

            <!-- Wind speed -->
            <StackPanel Grid.Column="1" Orientation="Horizontal" 
                                        HorizontalAlignment="Center">
                <!-- Icon -->
                <Viewbox Margin="5">
                    <Canvas Width="24" Height="24">
                        <Path Data="M4,10A1,1 0 0,1 3,9A1,1 0 0,1 4,8H12A2,
                                 2 0 0,0 14,6A2,2 0 0,0 12,
                                 4C11.45,4 10.95,4.22 10.59,4.59C10.2,5 9.56,
                                 5 9.17,4.59C8.78,4.2 8.78,
                                 3.56 9.17,3.17C9.9,2.45 10.9,2 12,2A4,4 0 0,
                                 1 16,6A4,4 0 0,1 12,10H4M19,
                                 12A1,1 0 0,0 20,11A1,1 0 0,0 19,10C18.72,
                                 10 18.47,10.11 18.29,10.29C17.9,
                                 10.68 17.27,10.68 16.88,10.29C16.5,9.9 16.5,
                                 9.27 16.88,8.88C17.42,8.34 18.17,
                                 8 19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14H5A1,
                                 1 0 0,1 4,13A1,1 0 0,1 5,12H19M18,
                                 18H4A1,1 0 0,1 3,17A1,1 0 0,1 4,16H18A3,
                                 3 0 0,1 21,19A3,3 0 0,1 18,22C17.17,
                                 22 16.42,21.66 15.88,21.12C15.5,20.73 15.5,
                                 20.1 15.88,19.71C16.27,19.32 16.9,
                                 19.32 17.29,19.71C17.47,19.89 17.72,20 18,
                                 20A1,1 0 0,0 19,19A1,1 0 0,0 18,18Z" 
                              Fill="#FF9B8C5E" />
                    </Canvas>
                </Viewbox>
                <!-- Speed -->
                <TextBlock Grid.Column="1" Style="{StaticResource WeatherTextStyle}"
                           Text="{Binding CurrentWeather.WindSpeed, 
                           StringFormat={}{0:F0} mps}"/>
            </StackPanel>
        </Grid>
    </Grid>
</UserControl>

The rest of the forecast data is displayed in an ItemsControl using a data template which shows the day of the forecast, weather icon, maximum and minimum temperatures, and wind speed.

XML
<DataTemplate x:Key="ForecastDataTemplate">
    <DataTemplate.Resources>
        <Storyboard x:Key="OnTemplateLoaded">
            <DoubleAnimationUsingKeyFrames
                            Storyboard.TargetProperty=
                            "(UIElement.RenderTransform).(TransformGroup.Children)[0].
                            (ScaleTransform.ScaleY)" 
                            Storyboard.TargetName="RootBorder">
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:1" Value="1">
                    <EasingDoubleKeyFrame.EasingFunction>
                        <QuinticEase EasingMode="EaseOut"/>
                    </EasingDoubleKeyFrame.EasingFunction>
                </EasingDoubleKeyFrame>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
    </DataTemplate.Resources>
    <Border x:Name="RootBorder" Height="200" Width="140" Margin="0,0,-1,0" 
     SnapsToDevicePixels="True" 
               Background="{StaticResource PrimaryDarkBrush}" 
               RenderTransformOrigin="0.5,0.5">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Border BorderThickness="1,1,1,0"
                       BorderBrush="{StaticResource PrimaryDarkBrush}" 
                       Background="{StaticResource PrimaryMidBrush}">
                <!-- Day of the week -->
                <TextBlock Style="{StaticResource WeatherTextStyle}" FontWeight="Normal"
                              Margin="5" Foreground="#FFADB982"
                              Text="{Binding Date, StringFormat={}{0:dddd}}"/>
            </Border>
            <!-- Weather icon -->
            <Border Grid.Row="1" BorderThickness="0,1,1,0"
                       BorderBrush="{StaticResource PrimaryDarkBrush}"
                       Background="{StaticResource PrimaryDarkBrush}">
                <Image MaxWidth="100" Margin="5,0,5,5">
                    <Image.Source>
                        <MultiBinding Converter="{StaticResource WeatherIconConverter}" 
                         Mode="OneWay">
                            <Binding Path="ID"/>
                            <Binding Path="IconID"/>
                        </MultiBinding>
                    </Image.Source>
                </Image>
            </Border>
            <!-- Min and max temperatures -->
            <Border Grid.Row="2" BorderThickness="1"
                       BorderBrush="{StaticResource PrimaryDarkBrush}"
                       Background="{StaticResource PrimaryLightBrush}">
                <TextBlock Grid.Row="0" Style="{StaticResource WeatherTextStyle}" 
                              Margin="5" Foreground="#FFAEBFAE">
                                <Run Text="{Binding MaxTemperature, StringFormat={}{0:F0}°}"/>
                                <Run Text="/" Foreground="Gray"/>
                                <Run Text="{Binding MinTemperature, StringFormat={}{0:F0}°}"/>
                </TextBlock>
            </Border>
            <Border Grid.Row="3" BorderThickness="1,0,1,1" 
                       BorderBrush="{StaticResource PrimaryDarkBrush}"
                       Background="{StaticResource PrimaryMidBrush}">
                <!-- Wind speed -->
                <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center">
                    <Viewbox Margin="0,5,5,5">
                        <Canvas Width="24" Height="24">
                            <Path Data="M4,10A1,1 0 0,1 3,9A1,1 0 0,1 4,8H12A2,2 0 0,0 14,6A2,
                                           2 0 0,0 12,4C11.45,4 10.95,4.22 10.59,
                                           4.59C10.2,5 9.56,5 9.17,
                                           4.59C8.78,4.2 8.78,3.56 9.17,3.17C9.9,
                                           2.45 10.9,2 12,2A4,4 0 0,
                                           1 16,6A4,4 0 0,1 12,10H4M19,12A1,
                                           1 0 0,0 20,11A1,1 0 0,0 19,
                                           10C18.72,10 18.47,10.11 18.29,
                                           10.29C17.9,10.68 17.27,10.68 16.88,
                                           10.29C16.5,9.9 16.5,9.27 16.88,
                                           8.88C17.42,8.34 18.17,8 19,8A3,
                                           3 0 0,1 22,11A3,3 0 0,1 19,14H5A1,
                                           1 0 0,1 4,13A1,1 0 0,1 5,12H19M18,
                                           18H4A1,1 0 0,1 3,17A1,1 0 0,1 4,
                                           16H18A3,3 0 0,1 21,19A3,3 0 0,1 18,
                                           22C17.17,22 16.42,21.66 15.88,
                                           21.12C15.5,20.73 15.5,20.1 15.88,
                                           19.71C16.27,19.32 16.9,19.32 17.29,
                                           19.71C17.47,19.89 17.72,20 18,
                                           20A1,1 0 0,0 19,19A1,1 0 0,0 18,18Z" 
                                     Fill="#FF9B8C5E" />
                        </Canvas>
                    </Viewbox>
                    <!-- Speed -->
                    <TextBlock Grid.Column="1" Style="{StaticResource WeatherTextStyle}" 
                                  Foreground="#FFAEBFAE"
                                  Text="{Binding WindSpeed, StringFormat={}{0:F0} mps}"/>
                </StackPanel>
            </Border>
        </Grid>
    </Border>
</DataTemplate>

The weather icons that are displayed are from VClouds and I'm using a converter to ensure the appropriate icon is displayed.

public class WeatherIconConverter : IMultiValueConverter
{
    public object Convert(object[] values, 
           Type targetType, object parameter, CultureInfo culture)
    {
        var id = (int)values[0];
        var iconID = (string)values[1];

        if (iconID == null) return Binding.DoNothing;

        var timePeriod = iconID.ToCharArray()[2]; // This is either d or n (day or night)
        var pack = "pack://application:,,,/OpenWeather;component/WeatherIcons/";
        var img = string.Empty;

        if (id >= 200 && id < 300) img = "thunderstorm.png";
        else if (id >= 300 && id < 500) img = "drizzle.png";
        else if (id >= 500 && id < 600) img = "rain.png";
        else if (id >= 600 && id < 700) img = "snow.png";
        else if (id >= 700 && id < 800) img = "atmosphere.png";
        else if (id == 800) img = (timePeriod == 'd') ? "clear_day.png" : "clear_night.png";
        else if (id == 801) img = (timePeriod == 'd') ? 
                            "few_clouds_day.png" : "few_clouds_night.png";
        else if (id == 802 || id == 803) img = (timePeriod == 'd') ? 
                              "broken_clouds_day.png" : "broken_clouds_night.png";
        else if (id == 804) img = "overcast_clouds.png";
        else if (id >= 900 && id < 903) img = "extreme.png";
        else if (id == 903) img = "cold.png";
        else if (id == 904) img = "hot.png";
        else if (id == 905 || id >= 951) img = "windy.png";
        else if (id == 906) img = "hail.png";

        Uri source = new Uri(pack + img);

        BitmapImage bmp = new BitmapImage();
        bmp.BeginInit();
        bmp.UriSource = source;
        bmp.EndInit();

        return bmp;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, 
                                object parameter, CultureInfo culture)
    {
        return (object[])Binding.DoNothing;
    }
}

Conclusion

While the OpenWeatherMap API has some limitations for free usage, it is still quite a suitable and well documented API. Hopefully, you have learnt something useful from this article and can now more easily go about using the API with your XAML applications.

History

  • 1st August, 2013: Initial post
  • 20th August, 2017: Updated code and article

License

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