C# Corner

Reactive Extensions: Just What the Doctor Ordered (Part 3)

In the final installment of this three-part series on Reactive Extensions for .NET, Eric Vogel shows how to put together all the pieces to create a working, reactive application.

Read the rest of the series: Part 1 | Part 2 | Part 3

In Part 1 of the series I went over the basics of Reactive Extensions (Rx). In Part 2 I went over how to compose observable sequences and how to observe asynchronous methods. For this final installment we will go over how to put all of the pieces together to create a full reactive application.

Our application will allow the user to enter a word or phrase and have it translated and optionally read in real-time. The user will be able to select both the source and destination language. We will be building upon the simple Bing Translation example that was developed in Part 2.

To get started create a new .NET 4.0 C# WPF Application. Open up MainWindow.xaml and place the following markup in the root <Grid> element's content.

MainWindow.xaml
<StackPanel>
            <StackPanel Orientation="Horizontal">
                <Label>From Language</Label>
                <ComboBox 
                    Name="cmbFromLang"
                    MinWidth="40"
                    ItemsSource="{Binding Path=RxVSMSampleP3.LinguaFranca.ViewModels.LanguageViewModel}"
                    SelectedValuePath="LanguageCode"
                    DisplayMemberPath="LanguageName"
                    SelectedItem="{Binding Path=Type}" 
                    ></ComboBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Label>To Language</Label>
                <ComboBox
                    Name="cmbToLang"
                    MinWidth="40"
                    Margin="14,0,0,0"
                    ItemsSource="{Binding Path=RxVSMSampleP3.LinguaFranca.ViewModels.LanguageViewModel}"
                    SelectedValuePath="LanguageCode"
                    DisplayMemberPath="LanguageName"
                    SelectedItem="{Binding Path=Type}" 
                    ></ComboBox>
            </StackPanel>
            <Label>Source</Label>
            <TextBox Name="txtSource"  Margin="4,4,4,4" MinHeight="60"></TextBox>
            <Label>Translated</Label>
            <TextBox Name="txtTranslated"  MinHeight="50" Margin="4,4,4,4"></TextBox>
            <CheckBox Name="chkSpeak">Speak translation</CheckBox>
            <StackPanel Orientation="Horizontal">
                <Label>Status:</Label>
                <Label Name="lblStatus"></Label>
            </StackPanel>
        </StackPanel>

Next add references to both System.Reactive and System.Reactive.Windows.Threading. We will be using the June 5th stable Rx release for the application. Now create two folders, one named ViewModels and one named Services. These folders will be using for storing our application's view models and service classes.

First let's add our LanguageViewModel class, which will be used for presenting our From and To Language combo boxes. Each language combo box will display a list of LanguageViewModel options, with the LanguageName as the display value and the LanguageCode as its selected value as we have defined in the XAML binding.

LanguageViewModel.cs
public class LanguageViewModel
 {
     public string LanguageCode { get; set; }
     public string LanguageName { get; set; }
 }

You should now be able to run the application and will be presented with the window below.


[Click on image for larger view.]
Figure 1.

Adding the Service Layer
Now that we have the UI settled for our application, let's get to the implementation of the core business logic. Create a TranslationService class within the Services folder and import the System.Reactive.Linq namespace. The TranslationService class will encapsulate all the translation logic needed for our application. We will be exposing all of the needed Bing Translation Web service methods as IObservable<T> sequences, to allow our application to orchestrate its symphony.

The first thing we need to do is add a service reference to the Bing Translation Service. Be sure to generate asynchronous operations when adding the reference. Refer to Part 2 of the series for full instructions on adding the Web service reference if needed. The implementation of the TranslationService is fairly straightforward. We maintain one instance of the Web Service Client to make all of our Web service calls and use the Observable.FromAsynchPattern factory method to wrap each asynchronous Web service method into its IObservable<T> counter-part. I wouldn't be too surprised if someone ends up creating a code gen for doing this task or for generating a web service proxy.

TranslationService.cs
 private BingTranslationService.LanguageServiceClient _proxy; 
 private const string AUDIO_FORMAT = "audio/wav";
 private const string TEXT_CONTENT_TYPE = "text/plain";
 private const string TEXT_CATEGORY = "general";
 private string _appId = "88E025E2D1E4C4CD930DD6C31AB82E3C49552CBB";

 public TranslationService()
 {
     _proxy = new BingTranslationService.LanguageServiceClient();
 }

 public IObservable<string[]> GetLanguageNames(string locale, string[] languageCodes)
 {
     string[] friendlyNames = new string[0];
     var getLanguageNames = Observable.FromAsyncPattern<string, string, string[], string[]>(_proxy.BeginGetLanguageNames,
                                                                                _proxy.EndGetLanguageNames);
     var result = getLanguageNames(_appId, locale, languageCodes);
     return result;
 }

 public IObservable<string[]> GetLanguagesForTranslate()
 {
     string[] supportedLangs = new string[0];
     var getLanguages = Observable.FromAsyncPattern<string, string[]>(_proxy.BeginGetLanguagesForTranslate,
                                                                      _proxy.EndGetLanguagesForTranslate);
     var result = getLanguages(_appId);
     return result;
 }

 public IObservable<string> GetTranslatedAudio(string text, string languageCode)
 {
     var speakTranslation = Observable.FromAsyncPattern<string, string, string, string, string>(_proxy.BeginSpeak, _proxy.EndSpeak);
     string audioURI = string.Empty;
     var result = speakTranslation(_appId, text, languageCode, AUDIO_FORMAT);
     return result;
 }

 public IObservable<string> Translate(string text, string fromLanguageCode, string toLanguageCode)
 {
     var getTranslation = Observable.FromAsyncPattern<string, string, string, string, string, string, string>(_proxy.BeginTranslate, _proxy.EndTranslate);
     var result = getTranslation(_appId, text, fromLanguageCode, toLanguageCode, TEXT_CONTENT_TYPE, TEXT_CATEGORY);
     return result;
 }

Putting it all together
Now that we have our translation service all prepared let's put it to good use. Open up MainWindow.xaml.cs add the following namespace imports.

using System.Reactive.Linq;
using System.Reactive.Threading;
using System.Reactive.Concurrency;
using RxVSMSampleP3.LinguaFranca.Services;
using RxVSMSampleP3.LinguaFranca.ViewModels;

Next we setup our local translation service, an instance of MediaPlayer, and store the current locale of the user's operating system.

private TranslationService _translationService;
private MediaPlayer _mediaPlayer;
private string _locale;

We start loading the combo boxes with the list of supported languages before the application is displayed to prevent user down-time once the application is fully loaded. We also subscribe to the Loaded event of the form to handle further user interaction once the application is fully loaded.

 public MainWindow()
 {
     _locale = System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
     InitializeComponent();
     _translationService = new TranslationService();
     _mediaPlayer = new MediaPlayer();
     this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
     LoadSupportedLanguages(_locale);
 }

To load the list of supported languages we utilize the IObservable<T>.Zip extension method, which allows us to join two Observable sequences and project a combinator. In this case we are merging the results of the language codes returned from GetLanguagesForTranslate and the language translations for each code returned by GetLanguageNames and projecting the result into an IEnumerabe<LanguageViewModel>. The resulting collection is then bound to the from and to language combo boxes through the LoadLanguageOptions call. We also update the status label to notify the user that we have retrieved languages that may be used for text translation. If an exception occurs, we display the exception message to the user.

private void LoadSupportedLanguages(string locale)
{
    var result = from lcs in _translationService.GetLanguagesForTranslate()
                 from l in _translationService.GetLanguageNames(locale, lcs)
                 select lcs.Zip(l, (x, y) => new LanguageViewModel { LanguageCode = x, LanguageName = y });

    var sub = result.ObserveOnDispatcher()
        .Subscribe(
    onNext: langs => LoadLanguageOptions(langs, locale, locale),
    onCompleted: () =>
    {
        lblStatus.Content = "Supported languages found";
    },
    onError: ex => { MessageBox.Show(ex)
 }
    );
}

private void LoadLanguageOptions(IEnumerable<LanguageViewModel> languages, string defaultFrom, string defaultTo)
 {
     cmbFromLang.ItemsSource = languages;
     cmbToLang.ItemsSource = languages;
     cmbFromLang.SelectedValue = defaultFrom;
     cmbToLang.SelectedValue = defaultTo;
 }

In the loaded event of the window we use the Observable.FromEventPattern extension method to observe the text changed event of the source text box. The FromEventPattern extension method uses reflection to find the given event for the given control. We also utilize the Throttle and DisctinctUtilChanged extension methods on IObservable<T>. The Throttle method takes a time span that is uses as a timer interval to poll the event. The DistinctUtilChanged extension method will filter to ensure that we only get a next text value when the text has actually changed. Next we subscribe to the composite text changed event and update the status label to display "Translation starting..." and begin the text translation in the OnNext event.

void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    int inputDelay = 750;

    var srcTextChanged = from evt in Observable.FromEventPattern(txtSource, "TextChanged")
                         select ((TextBox)evt.Sender).Text;

    var srcInput = srcTextChanged.Throttle(TimeSpan.FromMilliseconds(inputDelay))
                       .DistinctUntilChanged();

    srcInput.ObserveOnDispatcher().Subscribe(
        x =>
        {
            lblStatus.Content = "Translation starting...";
            TranslateText(x,cmbFromLang.SelectedValue.ToString(), cmbToLang.SelectedValue.ToString());
        });
}

To translate the text we subscribe to the Translation Service's Translate method. In the observer's OnNext method we store the translated text. In the OnError method we display any exceptions to the user, and in the OnCompleted method we update the translated text on the form and update the status label to display "Translation Completed." We also check if the Speak translation check box is checked and call the SpeakTranslation function to play the translated audio when it is checked.

private void TranslateText(string text, string fromLanguage, string toLanguage)
        {
            string translatedText = string.Empty;

            var sub = _translationService.Translate(text,fromLanguage,toLanguage)
                .ObserveOnDispatcher().Subscribe(
           t => translatedText = t,
           ex => MessageBox.Show(ex.Message),
           () =>
           {
               txtTranslated.Text = translatedText;
               lblStatus.Content = "Translation Completed";

               if (chkSpeak.IsChecked.Value)
               {
                   SpeakTranslation(translatedText, toLanguage);
               }
           });
        }

In the SpeakTranslation method, we check if there is any text to translate then retrieve the audio translation and play it. We subscribe to the GetTranslatedAudio Translation Service method and set the audio URI in the OnNext method. In the OnCompleted method we update the status label to display "Audio Translated", stop any previous audio, and play the translated track. In the OnError event of the observer we display the caught exception to the user.

private void SpeakTranslation(string text, string languageCode)
        {
            if (!string.IsNullOrWhiteSpace(text))
            {
                string audioURI = string.Empty;

                var sub = _translationService.GetTranslatedAudio(text, languageCode)
                    .ObserveOnDispatcher()
                .Subscribe(
                onNext: url => audioURI = url,
                onCompleted: () =>
                {
                    lblStatus.Content = "Audio translated";
                    _mediaPlayer.Close();
                    _mediaPlayer.Open(new System.Uri(audioURI));
                    _mediaPlayer.Play();
                },
                onError: ex =>
                {
                    MessageBox.Show(ex.Message);
                }
                );
            }
        }

[Click on image for larger view.]
Figure 2. The completed application

Conclusion
The Reactive Extensions library is quite extensive and has many practical applications, as has been demonstrated throughout the series. You should take Rx into consideration if you find that you are dealing with multiple asynchronous data sources or methods. Conversely Rx can add some unneeded complexity if you are only dealing with simple events or asynchronous methods that only return one value. Through its wide use of extension methods one can bridge the gap between Tasks, Events, and the multitude of asynchronous programming models in the .NET Framework.

comments powered by Disqus

Reader Comments:

Tue, Jul 19, 2011 Rob

I am assuming that Rx cannot be used in ASP.NET web-applications...is that correct?

Add Your Comments Now:

Your Name:(optional)
Your Email:(optional)
Your Location:(optional)
Comment:
Please type the letters/numbers you see above

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.