Я не совсем уверен, насколько хороши погодные приложения по умолчанию — те, что поставляются в комплекте с системой Windows Phone 7, но я всегда хотел разработать собственное приложение, которое отображало бы погоду в моем понимании. Для этого я довольно долго экспериментировал с Google Weather API . Хотя это и не задокументировано, это API, который прост в использовании и в то же время достаточно информативен.
Каждый вызов API выполняется через простой HTTP-запрос, который возвращает данные в формате XML. Сначала я думал о разработке пользовательского элемента управления, который бы отображал условия для определенного местоположения, но затем я решил просто перечислить их и передать пользовательский шаблон данных для каждого прогнозируемого дня.
Как правило, я пытался сделать пользовательский интерфейс очень простым — есть TextBlock, который будет показывать текущее местоположение, TextBox , который будет местом, где пользователь будет вводить местоположение, кнопка, которая будет запускать обновление, и ListBox, который будет содержать прогноз на ближайшие четыре дня.
Так сказать, XAML для главной страницы выглядит так:
<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>
Как видите, элемент управления ListBox привязан к коллекции. Я выбрал ObservableCollection , чтобы упростить привязку — мне не придется вручную повторно привязывать коллекцию к элементу управления. Фактическая коллекция также представлена DependencyProperty в основном классе:
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); }
}
Неизвестный класс здесь — WeatherElement — он используется для отображения деталей для каждого прогнозируемого дня. Класс состоит из пяти свойств, которые устанавливаются при создании экземпляра:
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; }
}
Свойство Icon также будет установлено на основе информации из Google Weather API — с каждым условием связано изображение. Однако с изображением есть одна проблема — она представлена в формате GIF-файла и не поддерживается в Silverlight. Конечно, есть обходной путь для этого, и я буду обсуждать это позже в этой статье.
Но теперь давайте посмотрим на основной метод, который будет извлекать данные, необходимые для заполнения 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);
}
}
Как видите, я отправляю обычный веб-запрос, который обрабатывается асинхронно. Затем я получаю текущие условия с помощью LINQ, чтобы выбрать только прогнозируемые условия (поскольку в возвращенном XML-файле также присутствует другой набор данных).
Вы, наверное, заметили, что перед добавлением WeatherElement в коллекцию я создаю новый веб-запрос для получения изображения. Единственная проблема, как я уже упоминал, заключается в том, что изображение в формате GIF.
Я создал сервис WCF (см. Эту статью о том, как это сделать), который будет конвертировать GIF в JPEG. По завершении OpenReadAsync запускает следующий обработчик событий:
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);
}
Здесь я ссылаюсь на созданную мной службу WCF (ссылка на службу была добавлена ранее) и вызываю еще одно асинхронное действие, которое преобразует изображение (метод зарегистрирован как часть службы):
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 является экземпляром BitmapImage, который доступен для всего класса, объявленного в заголовке класса. Когда асинхронная операция запущена, метод GetData ожидает, пока этот BitmapImage получит значение. Если он отличается от нуля , он добавляется в экземпляр WeatherElement ( свойство Icon ) и добавляется в коллекцию, связанную с ListBox .
Теперь перейдем к более простой части приложения — фактическим вызовам методов. Когда пользователь нажимает кнопку, вы хотите сбросить коллекцию и установить для настройки приложения новое значение — новое местоположение.
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);
}
}
Это установит заголовок страницы на новое место, а также сохранит местоположение в настройках приложения. Он также сбрасывает коллекцию, восстанавливая ее. Здесь вы можете сказать, что я мог связать свойство location со свойством Text для TextBlock с заголовком. Тем не менее, я использую этот вызов только в двух местах — когда пользователь нажал кнопку GET и когда приложение загружается:
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
if (IsolatedStorageSettings.ApplicationSettings["location"] != null)
{
PageTitle.Text = IsolatedStorageSettings.ApplicationSettings["location"].ToString();
Conditions = new ObservableCollection<WeatherElement>();
GetData();
}
}
Поэтому я оставил это без прямой привязки. Когда вы запускаете приложение, вы должны получить результат, подобный этому, учитывая, что служба конвертации GIF работает и Google Weather доступна.
В этой статье я не включил текущие условия, доступные через API. Вы можете реализовать это, просто добавив еще один запрос LINQ.
Причина, по которой я добавляю дополнительный слой преобразования изображений, заключается в том, чтобы не связывать целый новый набор значков — это потребует дополнительного пространства и ресурсов. Кроме того, прямое преобразование в приложении без вызова фактической службы приведет к значительному потреблению ресурсов, особенно на устройстве, где аппаратные средства имеют очень ограниченную производительность, поэтому целесообразно делегировать преобразование внешнему ресурсу.
Для экспериментов вы можете скачать тестовый проект здесь .
Сервисный (для конвертации) исходный код можно скачать здесь .