Я не совсем уверен, насколько хороши погодные приложения по умолчанию — те, что поставляются в комплекте с системой 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.
Причина, по которой я добавляю дополнительный слой преобразования изображений, заключается в том, чтобы не связывать целый новый набор значков — это потребует дополнительного пространства и ресурсов. Кроме того, прямое преобразование в приложении без вызова фактической службы приведет к значительному потреблению ресурсов, особенно на устройстве, где аппаратные средства имеют очень ограниченную производительность, поэтому целесообразно делегировать преобразование внешнему ресурсу.
Для экспериментов вы можете скачать тестовый проект здесь .
Сервисный (для конвертации) исходный код можно скачать здесь .