How to give an element focus when something happens?

I had a very simple problem: On loading of particular screen I wanted to give a TextBox focus. It’s freaking easy to do this in the code behind. Most of my applications are actually MVVM applications and in that case it’s not something for the ViewModel, it’s logic that belongs to the view. But how about a Behavior?

I want to give a specific control focus when something happens on something else. So that should be a TargetedTriggerAction<T>, T should be Control, because that’s something you can give focus. How easy can it be?

public class FocusOnEvent : TargetedTriggerAction<Control>
{
    protected override void Invoke(object parameter)
    {
        Target.Focus();
    }
}

Is that all? Yes it is, or actually, I want to make it even easier, add the default trigger using the DefaultTriggerAttribute. Default should be the Loaded event, so let’s add that single line.

[DefaultTrigger(typeof(FrameworkElement), typeof(EventTrigger), "Loaded")]
public class FocusOnEvent : TargetedTriggerAction<Control>
{
    protected override void Invoke(object parameter)
    {
        Target.Focus();
    }
}

Now we have this easy code, how do we use it? Just add a little bit of xaml to your page / control, just like any other behavior.

<i:Interaction.Triggers>
    <i:EventTrigger>
        <Behaviors:FocusOnEvent TargetName="TitleTextBox" />
    </i:EventTrigger>
</i:Interaction.Triggers>

Some second thoughts, is it required to take the Safe event Detachment base class for Windows Phone Behaviors by Joost van Schaik into consideration? No it’s not required, because we’re not manually attaching handlers to the events, that’s all done by the TargetedTriggerAction itself. Hope you guys think this little bit of code is useful.

Does Windows 8 support Gif images and Animated Gif images?

To some of us it may sound like a strange question, but developers which are used to Silverlight or Windows Phone development know why I ask this question. In both Silverlight and Windows Phone Gif images aren’t directly supported. Don’t ask me why, I don’t know why. Luckily Silverlight and Windows Phone developers are helped by an open source project called Image Tools which enables support for Gif and Animated Gif.

Windows 8 – HTML5 + JS

For a starter I just downloaded an ordinary gif-image from the internet and with no knowledge about how to add an image to a Windows 8 application I used my web-knowledge.

<img src="/images/window.gif" />

And yes it does work. What about an animated gif? So I recall an interesting animated gif on Shawn Wildermuth’s blog, his head-shot. So without do anything else than changing the source of the image tag I created it started working. A surprise? Not really I already heard that the rendering engine for Windows 8 applications that are build in HTML5 + JS is based on IE, so this is what I did expect.

Windows 8 – XAML + C#

So I used my Silverlight knowledge to do in XAML what I did in HTML5, add an image.

<Image Source="/Images/window.gif" />

A really fast surprise it started working in the design view of the xaml document even before I ran the project. And also after running the application it seems to work perfectly. And the animated gif? It appears in the designer as well, but doesn’t animate in there. Also when running the application the animation doesn’t work.

Just a quick thought: Silverlight doesn’t support gif at all, what about WPF? After some googling it seems that WPF does support gif images, but not really the animated gifs. But there are people who created solutions that should work to enable animated gifs in WPF. So let’s see how much of that is possible in Windows 8.

The solution for WPF makes use of the GifBitmapDecoder which isn’t there in Windows 8. But I found something that sounds similar the BitmapDecoder. I tried to get the a small proof-of-concept working, but failed so far. I invite you to solve this Animated Gif problem using the information I’ve given. Please share your solution with the readers of this post.

Conclusion

We all know WinRT isn’t finished, we are just looking at a developer preview. If I recall it correctly WinRT should have the same features in XAML+C# as in HTML5+JS. Will this small difference in features be tackled in the final release?

Statistics for your Windows Phone application (Google Analytics)

Alright, I’ve tried a couple of different systems to get the statistics for the apps I created.

Google Analytics custom way

I started more than a year ago with a unreliable option using Google Analytics. This option was unreliable because it didn’t handle things like no connectivity and was depending on the WebBrowser control. Technically this was an option, but there are better options.

Flurry Analytics

My first applications actually did go live with Flurry Analytics for the stats. This worked perfectly fine for some time, until I introduced a application version that only supported Mango. Strangely I never got stats for that version. In the end I heard more people complaining that the Flurry Analytics library didn’t work with Phones running on Mango. I was not happy because it took weeks before I got a response from Flurry about the problem. They asked if I was interested in doing some beta-testing. I honestly didn’t have interest in beta-testing after that long time. I hear some rumors that it’s still not working, but I’m not sure, I’ve given up Flurry Analytics.

PreEmptive Runtime Intelligence

Alright, then I heard people say, why don’t you use PreEmptive’s Runtime Intelligence. It’s free and actually an Enterprise Product. I tried it, and actually liked it. The documentation on how to use it was not top-level, but with a little bit of trying out I got things working. But even better the tool integrates an Obfuscation tool to obfuscate the code. But then it started to take longer to get the stats processed. The last time my stats were processed is at October 6th, that’s 13 days ago, way to long. I understand that it can take some time, 24 hours is pretty acceptable. But more, every user of Runtime Intelligence got an e-mail to say that the conditions are about to change in two months time. This is basically because Microsoft has changed the contract with PreEmptive, but still, the new conditions are not clear, yet. Will it be paid, will it be limited? We don’t know yet. So for me, it’s time to look for a solid Analytics solution that I know will be free and will be supported by tools in the market.

Google Analytics with Microsoft Silverlight Analytics Framework

Alright, I’ve chosen to go back to Google Analytics and follow the comments I got from my readers on my custom Google Analytics tracking post. They suggest to make use of Microsoft Silverlight Analytics Framework. This framework supports a couple of very important scenario’s.

  • Offline scenarios
  • Support for multiple analytics services, including:
    • Comscore
    • Google Analytics
    • PreEmptive Solutions

Where I already have experience with implementing analytics using the custom PreEmptive tools I wouldn’t try that again, how long will it be free? I also have some experience with Comscore for one of my customers. I don’t understand how to read the reports so that’s not really an option for me, neither is it free. So Google Analytics it is, I know how to read the reports, and I’ve got a lot of experience implementing it for websites. Let’s start with the implementation in a Windows Phone application.

Step 1 Downloading and Referencing

You can download the source or the installation package. Be aware that the source doesn’t contain the source code for the Analytics Services like Google Analytics. So recommended will be the download of the installation package. After the installation you can find the libraries in: C:\Program Files (x86)\Microsoft SDKs\Microsoft Silverlight Analytics Framework\

You’ll need to reference the following libraries when working with Google Analytics:

  • Microsoft.WebAnalytics.dll
  • Microsoft.WebAnalytics.Behaviors.dll
  • System.ComponentModel.Composition.dll
  • System.ComponentModel.Composition.Initialization.dll
  • System.Windows.Interactivity.dll

Note: I’ve had some trouble with conflicting System.Windows.Interactivity libraries. It’s distributed with Expression Blend, MVVM Light and also with the Microsoft Silverlight Analytics Framework. I think there needs to be a solution so that the distribution should no longer be required for the different frameworks. I specially had a conflict when using Microsoft Silverlight Analytics Framework (made for Windows Phone SDK 7.0) in combination with MVVM Light 4 Preview (made for Windows Phone SDK 7.1). I did fallback to the previous version of MVVM Light 3 to remove the conflict.

Step 2 Create the IApplicationService and use reference it in App.xaml

Let’s start with gathering some generic information I want to gather from my users. To be honest I want to do this for all my applications. So this static class get's some information about the device. The PhoneHelper class can be found inside the Coding4fun library which also has other nice stuff. You could use this also for difference between Trial and Paying users.

public static class AnalyticsProperties
{
    public static string DeviceId
    {
        get
        {
            var value = (byte[]) DeviceExtendedProperties.GetValue("DeviceUniqueId");
            return Convert.ToBase64String(value);
        }
    }

    public static string DeviceManufacturer
    {
        get { return DeviceExtendedProperties.GetValue("DeviceManufacturer").ToString(); }
    }

    public static string DeviceType
    {
        get { return DeviceExtendedProperties.GetValue("DeviceName").ToString(); }
    }

    public static string Device
    {
        get { return string.Format("{0} - {1}", DeviceManufacturer, DeviceType); }
    }

    public static string OsVersion
    {
        get { return string.Format("WP {0}", Environment.OSVersion.Version); }
    }

    public static string ApplicationVersion
    {
        get { return PhoneHelper.GetAppAttribute("Version").Replace(".0.0", ""); }
    }
}

So let’s continue with our IApplicationService that wraps the WebAnalyticsService provided by the Analytics Framework. Just to prevent that my applications start to depend to much on Microsoft Silverlight Analytics Framework, I don’t want to change too much code when I need or want to switch next time. I explicitly set the Page Tracking to false because I want to be in control when and what to track. Further more I’m setting the CustomVariables that you might recognize from Google Analytics. There is a maximum of 5 Custom Variables and because the GoogleAnalytics library also puts the ApplicationId inside the Custom Variables you end up having only four for your application. That’s one of the reasons why I have the Device Type and Manufacturer concatenated in the Device property, choose carefully what you want to track. Next is the interesting part of Initializing MEF, I’m not an expert on MEF but basically it’s a kind of IoC container but differently. We will make use of the composition possibilities that we get from using MEF in a moment. The last part is the WebPropertyId which is passed to Google Analytics, this is the Google Analytics Property Id which is in the form of UA-XXXXX-X. So make sure you register a Google Analytics Web Property

public class AnalyticsService : IApplicationService
{
    private readonly IApplicationService _innerService;
    private readonly GoogleAnalytics _googleAnalytics;

    public AnalyticsService()
    {
        _googleAnalytics = new GoogleAnalytics();
        _googleAnalytics.CustomVariables.Add(new PropertyValue { PropertyName = "Device ID", Value = AnalyticsProperties.DeviceId });
        _googleAnalytics.CustomVariables.Add(new PropertyValue { PropertyName = "Application Version", Value = AnalyticsProperties.ApplicationVersion });
        _googleAnalytics.CustomVariables.Add(new PropertyValue { PropertyName = "Device OS", Value = AnalyticsProperties.OsVersion });
        _googleAnalytics.CustomVariables.Add(new PropertyValue { PropertyName = "Device", Value = AnalyticsProperties.Device });
        _innerService = new WebAnalyticsService
                            {
                                IsPageTrackingEnabled = false,
                                Services = { _googleAnalytics, }
                            };
    }

    public string WebPropertyId
    {
        get { return _googleAnalytics.WebPropertyId; }
        set { _googleAnalytics.WebPropertyId = value; }
    }

    #region IApplicationService Members

    public void StartService(ApplicationServiceContext context)
    {
        CompositionHost.Initialize(
            new AssemblyCatalog(
                Application.Current.GetType().Assembly),
            new AssemblyCatalog(typeof(AnalyticsEvent).Assembly),
            new AssemblyCatalog(typeof(TrackAction).Assembly));
        _innerService.StartService(context);
    }

    public void StopService()
    {
        _innerService.StopService();
    }

    #endregion
}

You can then add this ApplicationService to the ApplicationLifetimeObjects in the App.xaml file.

<Application.ApplicationLifetimeObjects>
    <!--Required object that handles lifetime events for the application-->
    <shell:PhoneApplicationService Launching="ApplicationLaunching"
                                    Closing="ApplicationClosing"
                                    Activated="ApplicationActivated"
                                    Deactivated="ApplicationDeactivated" />
    <Analytics:AnalyticsService WebPropertyId="UA-XXXXX-X" />
</Application.ApplicationLifetimeObjects>

This also enables automatic tracking Launch, Closing, Activated and Deactivated events in Google Analytics. So we don’t have to do anything manually to enable that part.

image

Step 3 Start tracking usage of features

Besides the starting and stopping of your application you probably want to track the usage of features in your app. You can make use of the TrackAction Behavior for example which can be found in Microsoft.WebAnalytics.Behaviors. But I actually like to have this kind of stuff in code which makes it easier for me to get an overview of what I’m tracking. So I wrote this simple AnalyticsTracker class that makes use of MEF to import the WebAnalytics stuff.

public class AnalyticsTracker
{
    public AnalyticsTracker()
    {
        CompositionInitializer.SatisfyImports(this);
    }

    [Import("Log")]
    public Action<AnalyticsEvent> Log { get; set; }

    public void Track(string category, string name)
    {
        Track(category, name, null);
    }

    public void Track(string category, string name, string actionValue)
    {
        Log(new AnalyticsEvent { Category = category, Name = name, ObjectName = actionValue });
    }
}

Usage is pretty straightforward.

AnalyticsTracker tracker = new AnalyticsTracker();
tracker.Track("Advertisement", "Refreshed", adUnit);

Step 4 Analyse the data

Alright start with Top Events

image

Cool but in which countries?

image

Of course there much more analytics that you can see inside of Google Analytics, this is just a start.

Adding Tactile Feedback to your app the easy way

About a week ago there was a very good article with tips to make your wp7 app a killer app. Tip 24 was about tactile feedback. Tactile feedback can be something like 30 milliseconds running the VibrateController. That’s very easy, just like this:

VibrateController vibrateController = VibrateController.Default;
vibrateController.Start(new TimeSpan(0, 0, 0, 0, 30));

Because I just want to call one method instead of two lines of code, I created a small static method.

public static void GiveTactileFeedback()
{
    VibrateController vibrateController = VibrateController.Default;
    vibrateController.Start(new TimeSpan(0, 0, 0, 0, 30));
}

Alright, but now I had to add this method call to every Button Click, ListBox SelectionChanged, and ApplicationBar menu or button use. Probably there are some more places where I want to give a little bit of feedback. So I’m thinking about a solution, a generic solution. I remember how easy it was to apply the TiltEffect. So my mind was going the direction of a similar solution. So I started with some Attached Dependency Properties, the same mechanism that’s used to implement the TiltEffect. So enable it for example in the root of your Phone Page and only suppress the effect if you don’t want to apply it a special occasion.

<phone:PhoneApplicationPage ... 
                            WPUI:TactileFeedbackEffect.IsTactileFeedbackEnabled="true">

And suppress it in this way.

<Button Content="Feedback surpressed"
        WPUI:TactileFeedbackEffect.SuppressTactileFeedback="true" />

Basically I want to walk through all the children of the element that have the IsTactileFeedbackEnabled property set to true. So I started walking recursively through the Visual Tree.

public static IEnumerable<FrameworkElement> GetSelfAndChildren(this FrameworkElement node)
{
    var elements = new List<FrameworkElement>();
    int childrenCount = VisualTreeHelper.GetChildrenCount(node);
    elements.Add(node);
    for (int i = 0; i < childrenCount; i++)
    {
        var childFe = VisualTreeHelper.GetChild(node, i) as FrameworkElement;
        if (childFe != null)
        {
            elements.AddRange(GetSelfAndChildren(childFe));
        }
    }
    return elements;
}

Using this Visual Tree of elements to check against controls that should support the Tactile Feedback: ButtonBase (root of all buttons), Selector (root of the different types of ListBoxes), PhoneApplicationPage (only way to get a hold of the  ApplicationBar’s buttons and menu items). For the menu items and buttons, it’s just simply attaching to the Click event. The Selector has a SelectionChanged event to which we can listen. Not that much of rocket science.

The initial idea I had was to make use of the Loaded event of the Element that has  the DependencyProperty applied to start walking though the visual tree. But what happened when the Visual Tree changes? Caused for example by Data Binding changes, or any of the other ways that enable you to manipulate the Visual Tree. So I attached to the LayoutUpdated event as well, I had to hack a bit, because the sender in the LayoutUpdated event is always null.

In het end I came to the conclusion that I didn’t have a way to suppress feedback on the ApplicationBar items.This is mainly because the ApplicationBar items are not inheriting from DependencyObject.

Full code is below for both the VisualTreeHelper and the TactileFeedbackEffect. Please let me know if you have any questions or improvements.

/// <summary>
/// Simple helpers for walking the visual tree
/// </summary>
internal static class TreeHelpers
{
    public static IEnumerable<FrameworkElement> GetSelfAndChildren(this FrameworkElement node)
    {
        var elements = new List<FrameworkElement>();
        int childrenCount = VisualTreeHelper.GetChildrenCount(node);
        elements.Add(node);
        for (int i = 0; i < childrenCount; i++)
        {
            var childFe = VisualTreeHelper.GetChild(node, i) as FrameworkElement;
            if (childFe != null)
            {
                elements.AddRange(GetSelfAndChildren(childFe));
            }
        }
        return elements;
    }
}

public class TactileFeedbackEffect : DependencyObject
{
    /// <summary>
    /// Set this Attached Dependency Property to true to enable tactile feedback on an element and it's children.
    /// </summary>
    public static readonly DependencyProperty IsTactileFeedbackEnabledProperty = DependencyProperty.RegisterAttached
        (
            "IsTactileFeedbackEnabled",
            typeof (bool),
            typeof (TactileFeedbackEffect),
            new PropertyMetadata(OnIsTactileFeedbackEnabledChanged)
        );

    /// <summary>
    /// Set this Attached Dependency Property to true to suppress tactile feedback on the element.
    /// </summary>
    public static readonly DependencyProperty SuppressTactileFeedbackProperty = DependencyProperty.RegisterAttached(
        "SuppressTactileFeedback",
        typeof (bool),
        typeof (TactileFeedbackEffect),
        new PropertyMetadata(false));

    static TactileFeedbackEffect()
    {
        TactileFeedbackItems = new List<Type>
                                    {typeof (ButtonBase), typeof (PhoneApplicationPage), typeof (Selector)};
    }

    private TactileFeedbackEffect()
    {
    }

    public static List<Type> TactileFeedbackItems { get; private set; }

    public static bool GetIsTactileFeedbackEnabled(DependencyObject source)
    {
        return (bool) source.GetValue(IsTactileFeedbackEnabledProperty);
    }


    public static void SetIsTactileFeedbackEnabled(DependencyObject source, bool value)
    {
        source.SetValue(IsTactileFeedbackEnabledProperty, value);
    }

    public static bool GetSuppressTactileFeedback(DependencyObject source)
    {
        return (bool) source.GetValue(SuppressTactileFeedbackProperty);
    }


    public static void SetSuppressTactileFeedback(DependencyObject source, bool value)
    {
        source.SetValue(SuppressTactileFeedbackProperty, value);
    }

    private static void OnIsTactileFeedbackEnabledChanged(DependencyObject target,
                                                            DependencyPropertyChangedEventArgs args)
    {
        if (target is FrameworkElement)
        {
            if ((bool) args.NewValue)
            {
                (target as FrameworkElement).Loaded += (s, e) =>
                                                            {
                                                                var senderDo = s as DependencyObject;
                                                                TryAttachFeedback(senderDo);
                                                                if (senderDo is FrameworkElement)
                                                                    (senderDo as FrameworkElement).LayoutUpdated +=
                                                                        (s2, e2) =>
                                                                            {
                                                                                TryDetachFeedback(senderDo);
                                                                                TryAttachFeedback(senderDo);
                                                                            };
                                                            };
            }
            else
            {
                TryDetachFeedback(target);
            }
        }
    }

    private static void TryAttachFeedback(DependencyObject target)
    {
        foreach (FrameworkElement element in (target as FrameworkElement).GetSelfAndChildren())
        {
            foreach (Type t in TactileFeedbackItems)
            {
                if (t.IsAssignableFrom(element.GetType()))
                {
                    if ((bool) element.GetValue(SuppressTactileFeedbackProperty) != true)
                    {
                        if (element is ButtonBase)
                        {
                            (element as ButtonBase).Click += TactileFeedbackEffectClick;
                        }
                        if (element is PhoneApplicationPage)
                        {
                            var page = element as PhoneApplicationPage;
                            if (page.ApplicationBar != null)
                            {
                                foreach (IApplicationBarMenuItem button in
                                    page.ApplicationBar.Buttons ?? new List<IApplicationBarMenuItem>())
                                {
                                    button.Click += TactileFeedbackEffectApplicationBarClick;
                                }
                                foreach (IApplicationBarMenuItem menuItem in
                                    page.ApplicationBar.MenuItems ?? new List<IApplicationBarMenuItem>())
                                {
                                    menuItem.Click += TactileFeedbackEffectApplicationBarClick;
                                }
                            }
                        }
                        if (element is Selector)
                        {
                            (element as Selector).SelectionChanged += TactileFeedbackEffectSelectionChanged;
                        }
                    }
                }
            }
        }
    }

    private static void TryDetachFeedback(DependencyObject target)
    {
        foreach (FrameworkElement element in (target as FrameworkElement).GetSelfAndChildren())
        {
            foreach (Type t in TactileFeedbackItems)
            {
                if (t.IsAssignableFrom(element.GetType()))
                {
                    if ((bool) element.GetValue(SuppressTactileFeedbackProperty) != true)
                    {
                        if (element is ButtonBase)
                        {
                            (element as ButtonBase).Click -= TactileFeedbackEffectClick;
                        }
                        if (element is PhoneApplicationPage)
                        {
                            var page = element as PhoneApplicationPage;
                            if (page.ApplicationBar != null)
                            {
                                foreach (IApplicationBarMenuItem button in
                                    page.ApplicationBar.Buttons ?? new List<IApplicationBarMenuItem>())
                                {
                                    button.Click -= TactileFeedbackEffectApplicationBarClick;
                                }
                                foreach (IApplicationBarMenuItem menuItem in
                                    page.ApplicationBar.MenuItems ?? new List<IApplicationBarMenuItem>())
                                {
                                    menuItem.Click -= TactileFeedbackEffectApplicationBarClick;
                                }
                            }
                        }
                        if (element is Selector)
                        {
                            (element as Selector).SelectionChanged -= TactileFeedbackEffectSelectionChanged;
                        }
                    }
                }
            }
        }
    }

    private static void TactileFeedbackEffectSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        GiveTactileFeedback();
    }

    private static void TactileFeedbackEffectApplicationBarClick(object sender, EventArgs e)
    {
        GiveTactileFeedback();
    }

    private static void TactileFeedbackEffectClick(object sender, RoutedEventArgs e)
    {
        GiveTactileFeedback();
    }

    public static void GiveTactileFeedback()
    {
        VibrateController vibrateController = VibrateController.Default;
        vibrateController.Start(new TimeSpan(0, 0, 0, 0, 30));
    }
}

Cache as Fallback using Reactive Extensions

In my previous article I wrote about how to use the Isolated Storage using Reactive Extensions. Let’s combine that with the ability to access online resources using Reactive Extensions.

What I want to achieve can be explained best by a picture. Basically if downloading of a resource fails it should fall back to the cache.

image

Let’s combine some code from the previous article.

First of all we want to download a resource from the internet, and if that’s successful we want to cache that data.

public static IObservable<MemoryStream> CacheAsFallback(Uri onlineResource, string cacheKey)
{
    IObservable<MemoryStream> observableOnlineResource = FromInternet(onlineResource);
    return observableOnlineResource
        .Select(stream =>
                    {
                        CacheResource(stream, cacheKey);
                        stream.Position = 0;
                        return stream;
                    });
}

Yes that’s all, it’s easy to combine Reactive Extensions (Rx) with Linq. So the above code will make sure that the resource that’s downloaded is cached before it’s returned to the caller. Please take a notion on the resetting of the stream position.

Catch an exception in Reactive Extensions

The next step is a little bit less seen in the Rx examples found on the web. Catch an exception and continue with a different Observable.

public static IObservable<TSource> Catch<TSource, TException>(
    this IObservable<TSource> source,
    Func<TException, IObservable<TSource>> handler
    )
    where TException : Exception

So Catch is an Extension method that accepts a Func<Exception, IObservable<T>>. In our case that would be an IObservable<MemoryStream>. If you want to catch only specific Exception types, you could do that as well, but I want to fall back in case of any error. Further more the FromCache Observable returns null when nothing is found. I want that to be translated to an Exception.

public static IObservable<MemoryStream> CacheAsFallback(Uri onlineResource, string cacheKey)
{
    IObservable<MemoryStream> observableOnlineResource = FromInternet(onlineResource);
    return observableOnlineResource
        .Select(stream =>
                    {
                        CacheResource(stream, cacheKey);
                        stream.Position = 0;
                        return stream;
                    })
        .Catch<MemoryStream, Exception>(
            exception => FromCache(cacheKey)
                                .Select(cacheStream =>
                                            {
                                                if (cacheStream == null)
                                                    throw new Exception("No cache");
                                                return cacheStream;
                                            }));
}

So now I can write below code to get a resource, and in case of network connectivity problems it will fallback to a cache value.

const string url = "http://mark.mymonster.nl/Content/photo.jpg";
IObservable<MemoryStream> observable = ObservableResource.CacheAsFallback(new Uri(url), "photo.jpg");
observable
    .ObserveOnDispatcher()
    .Subscribe(
        stream =>
            {
                if (stream != null)
                {
                    var image = new BitmapImage();
                    image.SetSource(stream);
                    TargetImage.Source = image;
                }
            },
        exc => Debug.WriteLine(string.Format("Oops: {0}", exc.Message)),
        () => Debug.WriteLine("Completed."));

Now let’s combine everything we have again, toggle below to view it.

public static class StreamExtensions
{
    public static MemoryStream ToMemoryStream(this Stream stream)
    {
        var br = new BinaryReader(stream);
        var outputStream = new MemoryStream();
        var buffer = new byte[1024];
        int cb;
        while ((cb = br.Read(buffer, 0, buffer.Length)) > 0)
        {
            outputStream.Write(buffer, 0, cb);
        }
        outputStream.Position = 0;
        return outputStream;
    }
}

public static class ObservableResource
{
    private const string CacheFolder = "cache";

    public static IObservable<MemoryStream> CacheAsFallback(Uri onlineResource, string cacheKey)
    {
        IObservable<MemoryStream> observableOnlineResource = FromInternet(onlineResource);
        return observableOnlineResource
            .Select(stream =>
                        {
                            CacheResource(stream, cacheKey);
                            stream.Position = 0;
                            return stream;
                        })
            .Catch<MemoryStream, Exception>(
                exception => FromCache(cacheKey)
                                    .Select(cacheStream =>
                                                {
                                                    if (cacheStream == null)
                                                        throw new Exception("No cache");
                                                    return cacheStream;
                                                }));
    }

    public static void CacheResource(MemoryStream toCache, string cacheKey)
    {
        string targetFile = Path.Combine(CacheFolder, cacheKey);
        using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication())
        {
            if (!isoFile.DirectoryExists(CacheFolder))
                isoFile.CreateDirectory(CacheFolder);
            if (isoFile.FileExists(targetFile))
                isoFile.DeleteFile(targetFile);
            using (IsolatedStorageFileStream outputStream = isoFile.CreateFile(targetFile))
            {
                toCache.WriteTo(outputStream);
            }
        }
    }

    public static IObservable<MemoryStream> FromCache(string cacheKey)
    {
        return new AnonymousObservable<MemoryStream>(
            observer =>
                {
                    string targetFile = Path.Combine(CacheFolder, cacheKey);

                    using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication())
                    {
                        if (!isoFile.FileExists(targetFile))
                        {
                            observer.OnNext(null);
                        }
                        else
                        {
                            using (IsolatedStorageFileStream inputStream =
                                isoFile.OpenFile(targetFile, FileMode.Open, FileAccess.Read))
                            {
                                observer.OnNext(inputStream.ToMemoryStream());
                            }
                        }
                    }
                    observer.OnCompleted();
                    return Disposable.Empty;
                });
    }

    public static IObservable<MemoryStream> FromInternet(Uri onlineResource)
    {
        return
            new AnonymousObservable<WebResponse>(
                observer =>
                    {
                        var httpWebRequest =
                            (HttpWebRequest) WebRequest.Create(onlineResource);
                        httpWebRequest.BeginGetResponse(
                            iar =>
                                {
                                    WebResponse response;
                                    try
                                    {
                                        var requestState = (HttpWebRequest) iar.AsyncState;
                                        response = requestState.EndGetResponse(iar);
                                    }
                                    catch (Exception exception)
                                    {
                                        observer.OnError(exception);
                                        return;
                                    }
                                    observer.OnNext(response);
                                    observer.OnCompleted();
                                }, httpWebRequest);
                        return Disposable.Empty;
                    })
                .Select(
                    response => response.GetResponseStream().ToMemoryStream());
    }
}