Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Педро Ламас (Pedro Lamas)
Учитывая отзывы пользователей о приложении Currency Converter v2, пришло время его немного усовершенствовать!
Вот лишь некоторые комментарии, полученные нами от пользователей:
- Конвертация валют выполняется слишком медленно.
- Чрезмерный трафик данных в приложении/необходимость кэширования валютных курсов.
- Некоторые валюты не поддерживаются.
- Неточные результаты/устаревшие данные о валютных курсах.
Эти комментарии означают, что нам нужен более эффективный источник данных и некоторый механизм кэширования…
Включаю кофеварку и… поехали!
Bing — быть или не быть? Вот в чем вопрос…
В первой версии Currency Converter для конвертации валют использовался поисковик Bing. О результатах вы уже прочитали выше.
В текущей версии в качестве источника данных мы выбрали MSN Money, поскольку он содержит более актуальные и точные данные и работает с любыми валютами.
Запустите Internet Explorer 8.0+ и перейдите на страницу https://moneycentral.msn.com/investor/market/exchangerates.aspx. Здесь выводятся актуальные валютные курсы для доллара США.
На этой странице есть вся необходимая информация для перевода любой валюты в доллары США и наоборот. Кроме того, можно конвертировать валюту X в доллары США, а затем в валюту Y.
Так почему бы не получить все эти данные одним запросом, кэшировать их, а затем использовать для конвертации валют офлайн?
Как и прежде, для извлечения необходимых данных со страницы HTML мы воспользуемся регулярными выражениями. Для этого откройте Internet Explorer Developer Tools (нажмите <F12>), выберите «Select element by click» (Выбор элемента по щелчку) (<Ctrl> + <B>) и щелкните «Argentine Peso» (Аргентинский песо). Страница будет выглядеть примерно так:
Используя приведенную выше информацию, мы сможем увидеть шаблон в коде:
HTML
<tr>
<td>CURRENCY</td>
<td style=”text-align:right”><a SOMETHING>VALUE_IN_USD</a></td>
<td style=”text-align:right”><a SOMETHING>VALUE_PER_USD</a></td>
</tr>
Зная шаблон, мы сможем создать приведенное ниже регулярное выражение:
C#
private static Regex _resultRegex =
new Regex("<tr><td>(?<currency>[^<>]+)</td><td style=""text-align:right"">.*?>(?<value>[0-9.,]+)</a></td></tr>");
Применив это регулярное выражение к нужному HTML-коду, мы получим все соответствующие строки, включая наименование валюты и обменный курс для доллара США.
Пора кодировать
Теперь, когда мы знаем, как извлечь все валютные курсы с одного URL, настало время внести изменения в код, чтобы воспользоваться новыми данными.
Как и в предыдущей статье, мы воспользуемся шаблоном MVVM и покажем процесс кодирования с низшего (Model) до самого верхнего (View) уровня шаблона.
Изменения на уровне Model
Для того чтобы воспользоваться полученными и кэшированными валютными курсами, мы должны внести следующие изменения в нашу модель:
- задать для каждой валюты сохранение ее курса и последнего обновления;
- пометить одну валюту как базовую (доллар США), присвоив ей обменный курс 1,0 (на случай конвертации долларов в доллары);
- добавить в службу операцию «Обновить валютные курсы».
А вот и полная модель (изменения выделены желтым):
C#
using System;
public interface ICurrencyExchangeService
{
ICurrency[] Currencies { get; }
ICurrency BaseCurrency { get; }
void ExchangeCurrency(double amount, ICurrency fromCurrency, ICurrency toCurrency, Action<ICurrencyExchangeResult> callback);
void UpdateCachedExchangeRates(Action<CachedExchangeRatesUpdateResult> callback, object state);
}
public interface ICurrency
{
string Name { get; }
double CachedExchangeRate { get; set; }
DateTime CachedExchangeRateUpdatedOn { get; set; }
}
public interface ICurrencyExchangeResult
{
Exception Error { get; }
string ExchangedCurrency { get; }
double ExchangedAmount { get; }
}
public interface ICachedExchangeRatesUpdateResult
{
Exception Error { get; }
object State { get; }
}
Теперь в ICurrencyExchangeService есть новое свойство BaseCurrency, которому присвоено значение экземпляра валюты «US Dollar», а также метод UpdateCachedExchangeRates для обновления всех валютных курсов.
Для ICurrency добавилось два новых свойства: CachedExchangeRate для хранения обменного курса валюты и CachedExchangeRateUpdatedOn для даты последнего обновления.
Также был добавлен новый интерфейс ICachedExchangeRatesUpdateResult, возвращающий исключение при асинхронном выполнении метода ICurrencyExchangeService.UpdateCachedExchangeRates.
Посмотрим, как реализован интерфейс:
Прежде всего, нужно отметить, что у нас появился абстрактный класс CurrencyBase. Тем самым мы расширяем класс MsnMoneyCurrency, добавляя отдельное свойство Id для хранения числового идентификатора валюты, получаемого с MSN Money.
Затем добавился метод MsnMoneyV2CurrencyExchangeService, который является прямой реализацией ICurrencyExchangeService.
Обратите внимание, что в отличие от метода BingCurrencyExchangeService из предыдущей версии, метод MsnMoneyV2CurrencyExchangeService не расширяет класс CurrencyExchangeServiceBase, а только запрашивает онлайновые данные в методе UpdateCachedExchangeRates и не при каждом вызове метода ExchangeCurrency.
Ниже приведен код для этих классов:
C#
public class MsnMoneyV2CurrencyExchangeService : ICurrencyExchangeService
{
private const string MsnMoneyUrl = "<a href='https://moneycentral.msn.com/investor/market/exchangerates.aspx?selRegion=1&selCurrency=1";'>https://moneycentral.msn.com/investor/market/exchangerates.aspx?selRegion=1&selCurrency=1";</a>
#region Static Globals
private static Regex _resultRegex = new Regex(@"<tr><td>(?<currency>[^<>]+)</td><td style=""text-align:right"">.*?>(?<value>[0-9.,]+)</a></td></tr>");
private static ICurrency[] _currencies = new ICurrency[]
{
//The currencies exposed by MSN Money will go here
};
#endregion
#region Properties
public ICurrency[] Currencies
{
get
{
return _currencies;
}
}
public ICurrency BaseCurrency
{
get;
protected set;
}
#endregion
public MsnMoneyV2CurrencyExchangeService()
{
BaseCurrency = Currencies.First(x => x.Name == "US Dollar");
}
public void ExchangeCurrency(double amount, ICurrency fromCurrency, ICurrency toCurrency, bool useCachedExchangeRates, Action<ICurrencyExchangeResult> callback, object state)
{
if (useCachedExchangeRates)
{
try
{
ExchangeCurrency(amount, fromCurrency, toCurrency, callback, state);
return;
}
catch
{
}
}
UpdateCachedExchangeRates(result =>
{
if (result.Error != null)
{
callback(new CurrencyExchangeResult(result.Error, state));
return;
}
try
{
ExchangeCurrency(amount, fromCurrency, toCurrency, callback, state);
}
catch (Exception ex)
{
callback(new CurrencyExchangeResult(ex, state));
}
}, state);
}
private void ExchangeCurrency(double amount, ICurrency fromCurrency, ICurrency toCurrency, Action<ICurrencyExchangeResult> callback, object state)
{
var fromExchangeRate = fromCurrency.CachedExchangeRate;
var toExchangeRate = toCurrency.CachedExchangeRate;
var timestamp = DateTime.Now;
if (fromCurrency == BaseCurrency)
fromExchangeRate = 1.0;
else
{
if (timestamp > fromCurrency.CachedExchangeRateUpdatedOn)
timestamp = fromCurrency.CachedExchangeRateUpdatedOn;
}
if (toCurrency == BaseCurrency)
toExchangeRate = 1.0;
else
{
if (timestamp > toCurrency.CachedExchangeRateUpdatedOn)
timestamp = toCurrency.CachedExchangeRateUpdatedOn;
}
if (fromExchangeRate > 0 && toExchangeRate > 0)
{
var exchangedAmount = amount / fromExchangeRate * toExchangeRate;
callback(new CurrencyExchangeResult(toCurrency, exchangedAmount, timestamp, state));
}
else
throw new Exception("Conversion not returned!");
}
public void UpdateCachedExchangeRates(Action<CachedExchangeRatesUpdateResult> callback, object state)
{
var request = HttpWebRequest.Create(MsnMoneyUrl);
request.BeginGetResponse(ar =>
{
try
{
var response = (HttpWebResponse)request.EndGetResponse(ar);
if (response.StatusCode == HttpStatusCode.OK)
{
string responseContent;
using (var streamReader = new StreamReader(response.GetResponseStream()))
{
responseContent = streamReader.ReadToEnd();
}
foreach (var match in _resultRegex.Matches(responseContent).Cast<Match>())
{
var currencyName = match.Groups["currency"].Value.Trim();
var currency = Currencies.FirstOrDefault(x => string.Compare(x.Name, currencyName, StringComparison.InvariantCultureIgnoreCase) == 0);
if (currency != null)
{
currency.CachedExchangeRate = double.Parse(match.Groups["value"].Value, CultureInfo.InvariantCulture);
currency.CachedExchangeRateUpdatedOn = DateTime.Now;
}
}
callback(new CachedExchangeRatesUpdateResult(ar.AsyncState));
}
else
{
throw new Exception(string.Format("Http Error: ({0}) {1}",
response.StatusCode,
response.StatusDescription));
}
}
catch (Exception ex)
{
callback(new CachedExchangeRatesUpdateResult(ex, ar.AsyncState));
}
}, state);
}
}
Он работает следующим образом: при вызове метода ExchangeCurrency мы передаем параметр (useCachedExchangeRates), который диктует методу использовать (или не использовать!) кэшированные ранее валютные курсы.
Затем выполняется конвертация валюты и возвращаются результаты. Если операция генерирует исключение или если мы запретили использовать кэшированные валютные курсы, вызывается метод UpdateCachedExchangeRates для обновления валютных курсов и выполнения конвертации с новыми данными.
С моделью на этом все!
ViewModel
Мы полностью сохранили ViewModel предыдущей версии, но добавили новую функциональность. Ниже приведен код:
C#
public class MainViewModel : INotifyPropertyChanged
{
//Full previous code
#region Properties
[IgnoreDataMember]
public ICurrencyExchangeResult Result
{
get
{
return _result;
}
protected set
{
if (_result == value)
return;
_result = value;
RaisePropertyChanged("Result");
RaisePropertyChanged("ExchangedCurrency");
RaisePropertyChanged("ExchangedAmount");
RaisePropertyChanged("ExchangedTimeStamp");
}
}
[IgnoreDataMember]
public string ExchangedTimeStamp
{
get
{
if (_result == null)
return string.Empty;
return string.Format("Data freshness:\n{0} at {1}",
_result.Timestamp.ToShortDateString(),
_result.Timestamp.ToShortTimeString());
}
}
[DataMember]
public CurrencyCachedExchangeRate[] CurrenciesCachedExchangeRates
{
get
{
return Currencies
.Select(x => new CurrencyCachedExchangeRate()
{
CurrencyIndex = Array.IndexOf(Currencies, x),
CachedExchangeRate = x.CachedExchangeRate,
CachedExchangeRateUpdatedOn = x.CachedExchangeRateUpdatedOn
})
.ToArray();
}
set
{
foreach (var currencyData in value)
{
if (currencyData.CurrencyIndex >= Currencies.Length)
continue;
var currency = Currencies[currencyData.CurrencyIndex];
currency.CachedExchangeRate = currencyData.CachedExchangeRate;
currency.CachedExchangeRateUpdatedOn = currencyData.CachedExchangeRateUpdatedOn;
}
}
}
#endregion
//Full previous code
public void ExchangeCurrency()
{
if (Busy)
return;
BusyMessage = "Exchanging amount...";
_currencyExchangeService.ExchangeCurrency(_amount, _fromCurrency, _toCurrency, true, CurrencyExchanged, null);
}
public void UpdateCachedExchangeRates()
{
if (Busy)
return;
BusyMessage = "Updating cached exchange rates...";
_currencyExchangeService.UpdateCachedExchangeRates(ExchangeRatesUpdated, null);
}
private void CurrencyExchanged(ICurrencyExchangeResult result)
{
InvokeOnUiThread(() =>
{
Result = result;
BusyMessage = null;
if (result.Error != null)
{
if (System.Diagnostics.Debugger.IsAttached)
System.Diagnostics.Debugger.Break();
else
MessageBox.Show("An error has ocorred!", "Error", MessageBoxButton.OK);
}
});
}
private void ExchangeRatesUpdated(ICachedExchangeRatesUpdateResult result)
{
InvokeOnUiThread(() =>
{
BusyMessage = null;
Save();
if (result.Error != null)
{
if (System.Diagnostics.Debugger.IsAttached)
System.Diagnostics.Debugger.Break();
else
MessageBox.Show("An error has ocorred!", "Error", MessageBoxButton.OK);
}
});
}
private void InvokeOnUiThread(Action action)
{
var dispatcher = System.Windows.Deployment.Current.Dispatcher;
if (dispatcher.CheckAccess())
action();
else
dispatcher.BeginInvoke(action);
}
#region Auxiliary Classes
public class CurrencyCachedExchangeRate
{
[DataMember]
public int CurrencyIndex { get; set; }
[DataMember]
public double CachedExchangeRate { get; set; }
[DataMember]
public DateTime CachedExchangeRateUpdatedOn { get; set; }
}
#endregion
}
Прежде всего, вы, наверное, заметили новое свойство «только для чтения» ExchangedTimeStamp, которое передает в интерфейс строку данных с информацией о том, когда были получены используемые данные о валюте. Интерфейс получает уведомление о том, что значение этого свойства изменяется при изменении свойства Result.
Ниже мы видим еще одно новое свойство CurrenciesCachedExchangeRates, в котором хранятся кэшированные валютные курсы. Для того чтобы заставить его работать, у нас есть вспомогательный класс CurrencyCachedExchangeRate, в котором хранится валютный индекс, валютный курс и метка времени обновления.
Благодаря методу UpdateCachedExchangeRates пользователи могут принудительно вручную обновлять кэшированные валютные курсы.
Функции обратного вызова CurrencyExchanged и ExchangeRatesUpdated используют метод InvokeOnUiThread для проверки правильности выполнения своего кода в потоке UI.
View
Мы внесли два простых изменения в MainPage.xaml (наш главный View): была добавлена область экрана, отображающая метку времени для результата конвертации, и пункт меню для полного обновления валютных курсов.
Чтобы внести первое изменение, добавьте простую текстовую область TextArea внизу StackPanel и создайте ее привязку к свойству ExchangedTimeStamp из ViewModel:
XAML
<StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock Margin="12,0,0,-5" Style="{StaticResource PhoneTextSubtleStyle}">Amount</TextBlock>
<TextBox InputScope="TelephoneNumber" Text="{Binding Amount, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}" />
<TextBlock Margin="12,10,0,-5" Style="{StaticResource PhoneTextSubtleStyle}">From</TextBlock>
<toolkit:ListPicker ItemsSource="{Binding Currencies}" SelectedItem="{Binding FromCurrency, Mode=TwoWay}" FullModeHeader="FROM CURRENCY" Style="{StaticResource CurrencyListPicker}" />
<TextBlock Margin="12,10,0,-5" Style="{StaticResource PhoneTextSubtleStyle}">To</TextBlock>
<toolkit:ListPicker ItemsSource="{Binding Currencies}" SelectedItem="{Binding ToCurrency, Mode=TwoWay}" FullModeHeader="TO CURRENCY" Style="{StaticResource CurrencyListPicker}" />
<StackPanel>
<TextBlock Style="{StaticResource PhoneTextGroupHeaderStyle}" Text="{Binding ExchangedCurrency}"></TextBlock>
<TextBlock Margin="25, 0, 0, 0" Style="{StaticResource PhoneTextTitle1Style}" Text="{Binding ExchangedAmount}"></TextBlock>
<TextBlock Style="{StaticResource PhoneTextSubtleStyle}" Text="{Binding ExchangedTimeStamp}" TextWrapping="Wrap" TextAlignment="Right"></TextBlock>
</StackPanel>
</StackPanel>
Что касается пункта меню для обновления валютных курсов, добавьте новый элемент ApplicationBarMenuItem в коллекцию MenuItems, задайте подходящий текст и добавьте обработчик для события щелчка:
XAML
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
<shell:ApplicationBarIconButton IconUri="/Images/appbar.money.usd.png" Text="Exchange" Click="ExchangeIconButton_Click" />
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text="update exchange rates" Click="UpdateExchangeRatesMenuItem_Click" />
<shell:ApplicationBarMenuItem Text="about" Click="AboutMenuItem_Click" />
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
Теперь осталось реализовать метод UpdateExchangeRatesMenuItem_. Для этого щелкните обработчик событий в MainPage.xaml.cs:
C#
private void UpdateExchangeRatesMenuItem_Click(object sender, EventArgs e)
{
var viewModel = DataContext as MainViewModel;
if (viewModel == null)
return;
Dispatcher.BeginInvoke(() =>
{
viewModel.UpdateCachedExchangeRates();
});
}
Заключение
В результате мы получили приложение, по качеству не уступающее используемому источнику данных. Благодаря новому (и более качественному) источнику данных и нескольким простым изменениям кода, наш Currency Converter стал работать как никогда быстро.
И как раз вовремя — кофе готов!
Об авторе
Педро Ламас (Pedro Lamas) родом из Португалии. Педро имеет статус .NET Senior Developer и работает в компании-партнере Microsoft DevScope, используя все мощные возможности платформы Microsoft .NET для разработчиков.
Педро также работает администратором сети PocketPT.net, крупнейшего сообщества Windows Phone в Португалии, оказывая активную поддержку разработчикам под Windows Phone, и выступает в качестве докладчика на мероприятиях Microsoft в Португалии, посвященных разработке на базе Windows Phone.