Windows 7 Currency Converter v2 – Now on Caffeine!

Discussion in 'Live RSS Feeds' started by News, May 2, 2011.

  1. News

    News Extraordinary Robot
    News Feed

    Joined:
    Jun 27, 2006
    Messages:
    26,205
    Likes Received:
    20
    Taking user feedback about the application into consideration, it’s time to make some improvements! J
    Here are some of the reports we got from the users:
    • The application is too slow when exchanging currencies
    • It uses too much data traffic/should cache the exchange rates
    • It doesn’t work for some currencies
    • The results are inaccurate/using out-of-date exchange rates
    So, what we can see from these comments is that we need a better data source, and that we should use some sort of caching mechanism…
    I think I’ll go ahead and put some coffee on to boil!
    [h=3]To Bing or not to Bing…[/h]The first version of Currency Converter used Bing to make the exchanges, which resulted in some of the reports you read above!
    For this version, however, we decided to use MSN Money because it has more accurate and up-to-date data, and because it works every time no matter the currency!
    MSN Money provides a very nice page on which we can see current currency exchange rates in relation to US Dollars; just open your Internet Explorer 8.0+ and navigate to the following URL:
    http://moneycentral.msn.com/investor/market/exchangerates.aspx
    [​IMG]
    As you can see here, we have all the data we need to convert from X to USD and from USD to X, and we can even convert from X to USD to Y.
    So, why not just get all of this data on a single request, cache it, and use it offline to make the currency exchanges? J
    Like before, we will retrieve the data we require from the page HTML by using Regular Expressions. To do so, open Internet Explorer Developer Tools (press F12), use the “Select element by click” option (Ctrl + B), and click on the “Argentine Peso” text; you’ll get something looking like this:
    [​IMG]
    Using the information above, we can see a pattern in the code:

    HTML

    [TR]
    [TD]CURRENCY[/TD]
    [TD]VALUE_IN_USD[/TD]
    [TD]VALUE_PER_USD[/TD]
    [/TR]
    Now that we know the pattern, we are now able to build this regular expression:

    C#

    private static Regex _resultRegex = new Regex("[TR]
    [TD](?[^]+)[/TD]
    [TD].*?>(?[0-9.,]+)[/TD]
    [/TR]
    ");Applying this Regular Expression to the retrieved HTML will allows us to get every row matching it, and retrieve the Currency Name and the “Per USD” Exchange Rate!
    [h=3]Time for some coding[/h]Now that we know how to get all the currency rates from a single URL, it’s time to make the necessary changes to our code to accommodate the new data!
    Like the previous article, we will maintain the MVVM pattern, showing the coding from the pattern’s bottom (Model) to the very top (View).
    [h=4]The (Re)Model[/h]Here are the changes we need to make on our model in order to accommodate the retrieved and cached currency rates:
    • Set each currency to save its exchange rate and last update
    • Mark the base currency (US Dollar), giving it an exchange rate of 1.0 (trying to convert from USD to USD? Right…)
    • Add an “Update Exchange Rates” operation to the service
    And here is the full Model, with the changes in yellow:
    [​IMG]

    C#

    using System;public interface ICurrencyExchangeService{ ICurrency[] Currencies { get; } ICurrency BaseCurrency { get; } void ExchangeCurrency(double amount, ICurrency fromCurrency, ICurrency toCurrency, Action callback); void UpdateCachedExchangeRates(Action 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; }}The ICurrencyExchangeService now has a new BaseCurrency property that we will set with the “US Dollar” currency instance, as well as an UpdateCachedExchangeRates method to update all the exchange rates.
    For the ICurrency, we have two new properties: the CachedExchangeRate to store the currency exchange rate value, and the CachedExchangeRateUpdatedOn for the last update date.
    A new interface called ICachedExchangeRatesUpdateResult has been added in order to return any exception thrown by the ICurrencyExchangeService.UpdateCachedExchangeRates method asynchronous execution to the caller.
    Now let’s look at the interface’s implementation:
    [​IMG]
    The first new thing to take note of is that we now have a CurrencyBase abstract class. From here, we extend the MsnMoneyCurrency class, adding a single Id property to store the numeric Id for the Currency found in MSN Money.
    Next is the MsnMoneyV2CurrencyExchangeService, a direct implementation of the ICurrencyExchangeService.
    Unlike BingCurrencyExchangeService from the previous version, notice that MsnMoneyV2CurrencyExchangeService does not extend the CurrencyExchangeServiceBase, and that it only requests online data in the UpdateCachedExchangeRates method and not on every ExchangeCurrency method call.
    Here is the code for these classes:

    C#

    public class MsnMoneyV2CurrencyExchangeService : ICurrencyExchangeService{ private const string MsnMoneyUrl = " #region Static Globals private static Regex _resultRegex = new Regex(@"[TR]
    [TD](?[^]+)[/TD]
    [TD].*?>(?[0-9.,]+)
    [/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 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 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 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()) { 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); }}Here’s how it works: when the ExchangeCurrency method is called, we pass a parameter (useCachedExchangeRates) that instructs the method to use (or not!) the previously cached exchange rates.
    Next, make the exchange operation and return the results. If the operation throws an exception, or if we didn’t allow for cached exchange rates usage, call the UpdateCachedExchangeRates to update the exchange rates and then run the exchange operation with the new data.
    And that’s about it for the Model!
    [h=4]The ViewModel[/h]We maintained the full ViewModel from the previous version, but added some new functionality to it. Here’s the coding (main changes are in yellow):

    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}The first thing you will notice here is a new ExchangedTimeStamp read-only property that feeds the interface with the date string to denote when the used currency data was obtained. The interface is notified that this property value has changed when the Result property value is also changed.
    Further down there’s another new property, CurrenciesCachedExchangeRates, that stores the cached exchange rates. For this to work, we have an auxiliary class called CurrencyCachedExchangeRate that stores the currency index along with the exchange rate as well as the update timestamp.
    The UpdateCachedExchangeRates method allows users to manually force an update over the cached exchange rates.
    The CurrencyExchanged and ExchangeRatesUpdated callbacks use the InvokeOnUiThread method to make sure that their codes run properly on the UI thread.
    [h=4]The View[/h]Two simple changes have been made in the MainPage.xaml (our main View): an area on the screen has been added to show the exchange operation result timestamp, and a menu option has been added to force a full exchange rate update.
    To make the first change, add a simple TextArea on the bottom StackPanel and bind it to the ExchangedTimeStamp property of the ViewModel:

    XAML

    Amount From To As for the “update exchange rates” menu option, add a new ApplicationBarMenuItem to the MenuItems collection, set the appropriate text, and add a handler for the click event:

    XAML


    Now, all that is missing is implementing the UpdateExchangeRatesMenuItem_To do so, click the event handler in the 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(); });}[h=3]Conclusion[/h]The bottom line is that your application is as good as the data source you use. By utilizing a new (better) data source, some really simple changes to the code, we now have the Currency Converter—faster than ever!
    And just in time: the coffee is ready!
    [h=3]About The Author[/h]Pedro Lamas is a Portuguese .Net Senior Developer on Microsoft’s Partner DevScope, where he works with all the cool stuff that Microsoft .Net has to offer its developers!
    He’s also one of the administrators of PocketPT.net, the largest Windows Phone Portuguese community, where his contribution is mostly visible on support for Windows Phone developers, and as a speaker for Windows Phone Development in Microsoft Portugal Events.
    You can read his blog or contact him via twitter!
    [​IMG]

    More...
     

Share This Page

Loading...