Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Currency Converter v2 – Now on Caffeine!

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!

To Bing or not to Bing…

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

image

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:

image

Using the information above, we can see a pattern in the code:

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>

Now that we know the pattern, we are now able to build this regular expression:

C#

private static Regex _resultRegex = 
    new Regex("<tr><td>(?<currency>[^<>]+)</td><td style=""text-align:right"">.*?>(?<value>[0-9.,]+)</a></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!

Time for some coding

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).

The (Re)Model

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:

image

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; }
}

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:

image

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 = "http://moneycentral.msn.com/investor/market/exchangerates.aspx?selRegion=1&selCurrency=1";    #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);
    }
}

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!

The ViewModel

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.

The View

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

<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>

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

<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>

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();
    });
}

Conclusion

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!

About The Author

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!

Tags:

Follow the Discussion

  • Sasha K.Trancecoder better known as Arktronic

    Are you seriously advocating the use of screen-scraping? Not only is that notoriously unstable (one change in the MSN markup and your app stops working) but I'm pretty sure it violates the ToS for virtually all Microsoft web properties.

  • Actually, I think the opposite is true ... MS, as you likely know, is allowing devs to deeply integrate various services (without charge) in their paid apps .. Bing maps, translation etc ..  all you need is a free developer app key .. some 'REST' and odata based services are available too ... and a number of other companies (like twitter and Netflix and many others) are providing open javascript accessible apis that return Json or XML ...

    Your other point is valid .. it is a quite brittle approach ... but the fact is unless you own the data you always have the issue of somebody changing the api or format.

  • Pedro LamasPedro Lamas Pedro Lamas

    @Trancecoder, I do not advocate in using screen scraping (at least if it's done directly by the application and not through some controlled middle web-service), but your missing the point here: this is a beginner article on how to use build a small and pratical MVVM application for Windows Phone 7!

    It is not meant to be an excellent application, it's meant to be a good (I hope!) tutorial! Wink

  • redtitleredtitle

    Hi!Excuseme, where´s the source?

  • Pedro LamasPedro Lamas Pedro Lamas

    @redtitle: The download link was on the top right of the page, pointing to http://currency.codeplex.com/  Wink

  • Money Rate Exchange CalculatorMoney Rate Exchange Calculator

    this blog is good and nice and have more knowledge about conversion

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.