Taming events in Silverlight with Reactive Extensions (Rx) and MVVM
After a fun talk last night with my local Bay .NET User Group I needed to fix one of my demos that I did not show last night because I ran out of time (always too much content?). Here is the example of Rx and MVVM in action to tame events generated by text box in Silverlight.
The demo I wanted to show is centered around two Rx methods. First is the Observable.FromEvent and the second is the ability to throttle the events with Throttle(TimeSpan) extension methods.
What I wanted to do is to use the MVVM pattern with text box TextChanged event and be able to throttle how often the changed text is being sent to a Bing news API (which is very cool and simple) using web request/response.
The route I went uses attached behavior to the text box (as an example). This behavior is a staging place for Rx to convert the textbox textchanged event in the IObservable via Observable.FromEvent extension method .
In my view (XAML) I have attached the RxEventTrigger behavior to a text box (line 20).
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"
xmlns:local="clr-namespace:WebServiceCalls" xmlns:ee="http://schemas.microsoft.com/expression/2010/effects" x:Class="WebServiceCalls.MainPage"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<local:ViewModel />
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel>
<TextBox x:Name="Search" Width="190" Height="29" DataContext="{Binding}" >
<i:Interaction.Behaviors>
<local:RxEventTrigger RxProperty="{Binding TextChangedSource, Mode=TwoWay}" />
</i:Interaction.Behaviors>
</TextBox>
</StackPanel>
</Grid>
</UserControl>
The behavior implementation is pretty standard. Create a class and inherit from Behavior. I am bit cheating here and limiting this behavior to only textbox but you get an idea.
public class RxEventTrigger : Behavior<TextBox>
{
public static readonly DependencyProperty RxPropertyProperty =
DependencyProperty.Register("RxProperty", typeof(IObservable<string>),
typeof(RxEventTrigger), null);
public IObservable<string> RxProperty
{
get { return (IObservable<string>)GetValue(RxPropertyProperty); }
set { SetValue(RxPropertyProperty, value); }
}
protected override void OnAttached()
{
this.AssociatedObject.Loaded += AssociatedObject_Loaded;
}
void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
RxProperty = Observable.FromEvent<TextChangedEventHandler, TextChangedEventArgs>(
h => new TextChangedEventHandler(h),
h => AssociatedObject.TextChanged += h,
h => AssociatedObject.TextChanged -= h)
.Select(t => ((TextBox)t.Sender).Text)
.Throttle(TimeSpan.FromMilliseconds(400))
.SubscribeOnDispatcher();
}
}
Line 18 is where we hook into the TextChanged event stream (FromEvent) using Rx, select current text with the projection (Select), throttle user input, so we don’t get every letter they type (Throttle), and finally tell the observable that subscribers should be subscribed on the Dispatcher so we don’t have cross thread issues when the event subscription occurs (+= h, –= h).
The connection between the behavior and the view model happens when we set the RxProperty in the behavior class which is the property we received via binding from the VM that was passed into our view (line 12-14 in the XAML).
Finally our view model will combine the text source and web service calls that will occur every time user stops typing the text.
public class ViewModel : NotifyPropertyChangedBase
{
#region fields
private Repository _repository = new Repository();
private ObservableCollection<NewsArticle> _newsList;
private IObservable<string> _textChangedSource;
#endregion
public ViewModel()
{
NewsList = new ObservableCollection<NewsArticle>();
}
public IObservable<string> TextChangedSource
{
get { return _textChangedSource; }
set
{
_textChangedSource = value;
OnPropertyChanged(() => TextChangedSource);
Func<IObservable<int>> clearList = () =>
{
NewsList.Clear();
return Observable.Return(0);
};
var q = from text in _textChangedSource
from clear in Observable.Defer(clearList).SubscribeOnDispatcher()
from news in _repository.GetNews(text).Take(3).TakeUntil(_textChangedSource)
select news;
q.ObserveOnDispatcher().Subscribe(
r=> NewsList.Add(r),
ex=> Debug.WriteLine(ex.Message),
()=> Debug.WriteLine("Complete.")
);
}
}
public ObservableCollection<NewsArticle> NewsList
{
get { return _newsList; }
set
{
_newsList = value;
OnPropertyChanged(() => NewsList);
}
}
}
Now that we have IObservable for text changes we can combine it with the clearing the NewsList and calling the web service to get 3 articles per user text input all in one LINQ query.
I have introduced a function on the line 22 which clears the list before calling the service. Defer extensions method will call the clearList Func delegate every time new subscription is attached due to text being generated by the text changed event.
I think line 30 is pretty straight forward except the TakeUntil. Take until is acting as a valve that will ignore previous web service results if user has typed some new text into the text box.
For completes here is the GetNews method in the repository:
public class Repository
{
public IObservable<NewsArticle> GetNews(string word)
{
var url = "http://api.search.live.net/xml.aspx?Appid={0}&sources={1}&query={2}";
var completeUri = String.Format(url, "[your api key here]", "news", word);
XNamespace ns = "http://schemas.microsoft.com/LiveSearch/2008/04/XML/news";
var q = from v in Observable.Defer(() => Observable.Return(WebRequest.Create(completeUri)))
from e in Observable.FromAsyncPattern<WebResponse>(v.BeginGetResponse, v.EndGetResponse)()
from r in Observable.Return(XDocument.Load(e.GetResponseStream()))
from t in r.Descendants().Elements(ns + "NewsResult").ToObservable()
from i in Observable.Return(t.Descendants(ns + "Title").First().Value)
from g in Observable.Return(t.Descendants(ns + "Source").First().Value)
from o in Observable.Return(t.Descendants(ns + "Date").First().Value)
select new NewsArticle { Headline = i, Source = g, Date = DateTime.Parse(o)};
return q;
}
}
public struct NewsArticle
{
public string Headline { get; set; }
public string Url { get; set; }
public string Source { get; set; }
public DateTime Date { get; set; }
}
Reactive Extensions
Take time to study the Reactive Extensions (Rx) because its a very critical for your tool belt. Rx enables some complex scenarios and all but eliminates lot of +=, –= nonsense (event subscribe / unsubscribe). This is specially important in Silvelright development.
Here are the few good resources to get started from my Diigo account.
If I would to pick out some of the so many ways Rx can help, I would say that choosing appropriate scheduler (e.g. UI thread) to execute or subscribe, creating and manipulating time and consuming asynchronous operations is just a tip of the iceberg.
This is the main page to get the Rx. Rx libraries exist for Silverlight, .NET 4 and Javascript.
Go have fun.
My Azure Storage
Watching the Cloud Cover episode 17, Steve Marx shows a nice and simple cloud storage management, web based tool to manage Azure Storage https://myazurestorage.com/.
At the same time I was playing around with Cloud Storage Studio. I am not sure why these guys are charging $60 bucks for their Storage Studio product.