Tutorial: Building a connected phone app with AgFx

Earlier this week I wrote a post detailing the application framework that I’ve been working on, which I’m calling AgFx.  I wanted that post to be a bit of an introductory overview,. Now I’m going to dig into detail a bit more, as well as show a bit about how you can use the free Windows Phone 7 Developer tools to quickly create a great application.  AgFx does a lot, and one of the apps I used to generate requirements and testing, is Jeff Wilcox’s gorgeous new foursquare™ client: 4th & Mayor, which is built on AgFx. 

Jeff and I, along with some others, have written several apps on top of AgFx over the last few months, and have really sharpened it to be exactly what you need when building a connected phone application.  And the steps that you use to do it are almost always the same, and I’ll detail them here.

If you’re thinking about writing an app that talks to a web service of any kind, go ahead and follow along with that example in your head.

Below is pretty long and comprehensive, but it is not only a basic tutorial, it is also a summary of some of the broader set of features in AgFx.  So it may give you some ideas about how to use AgFx for your next Phone application, or even your current one!  We have moved several existing data-connected applications to AgFx and in every case the code becomes smaller, simpler, and better performing.

Step 1: Figure out what data you’ll be using

The first step is to go take a look at the data that the web service offers and how you can process it.  In this example case we’ll be using the NOAA National Weather Service REST Web Service

So we go to the website above an look at what the web service offers.  Looking through at their service operations, I see this one:

  • Summarized Data for One or More Zipcodes
  • : Returns DWML-encoded NDFD data for one ore more zip codes (50 United States and Puerto Rico).

Which sounds about right.  Give it a zip code, get back weather data.  And there is an example for a query there as well:

https://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?zipCodeList=98052&format=12+hourly&numDays=7

We like things that we can easily build a query string from.  AgFx supports more complex operations, but these GET queries are a slam dunk.  If you click on that link, you’ll get a bunch of XML data.  It’s a little hairy, but the data is there so all we need to do is parse it, which is easy enough.  OK, weather data: check.

One thing it does not give us, though, is the name of the city for the zip code.  It gives us the latitude and longitude, but that’s not that helpful really.  We want to be able to display the name of the city for a given zip code, and we can get that from WebServiceX.NET, here.

WebServiceX.net provides an API for converting a zip code into a city and state name.

Basically we call this:  https://www.webservicex.net/uszip.asmx/GetInfoByZIP?USZip=98052, and we get this:

 <?xml version="1.0" encoding="utf-8"?>
<NewDataSet>
  <Table>
    <CITY>Redmond</CITY>
    <STATE>WA</STATE>
    <ZIP>98052</ZIP>
    <AREA_CODE>425</AREA_CODE>
    <TIME_ZONE>P</TIME_ZONE>
  </Table>
</NewDataSet>

Perfect!  City name: check.

Step 2: Define your identifiers and LoadContexts

Now it’s time to start writing the code to talk to the service.

The first thing to think about is the shape of the objects coming back from the service, and what is their unique identifier.  In database terms, this would be the primary key.  Given that key, I expect to get the correct data back, and that data should be discreet from calls using different keys.

In the weather case, the weather data is basically a set of weather forecast objects.  They always come together as a group and this app will never talk to them individually – it’s the collection of objects that makes up the data we care about.  We don’t care about addressing the individual forecasts, so the zip code works great as the identifier for our forecast.

For the zip code information, it’s even easier, the data is tied directly to the zip code, so that’s also the unique identifier here.

Before we go on, let’s think of some other examples.

Imagine you were talking to the Flickr service.  Flickr has identifiers on almost every object.  For example the identifier for my user profile on Flickr is “78467353@N00”.  If I was to write a model for my Flickr user profile, I’d use that as my identifier.  The same goes for a model that represents my PhotoStream, because that’s tied to my user account.  But a model for an individual album or photo would use the id associated with that item.  And so on.

This identifer is how you’ll ask for data from AgFx, so that’s why it’s important, we’ll see more about this later:

 DataManager.Current.Load<ZipCodeVm>(txtZipCode.Text);

So, back to LoadContext what’s going on with that guy?  Good question.

The LoadContext is where I store data that will be used to make a request for my data.  For example, if I want to request a photos on Flickr from a photo album, I may want to pass in paging information, or information about how many items to return.  The LoadContext is where I would put that information, the reason why will be clear later.  LoadContext really exists for more sophisticated cases and in the case of simple requests, like those for the weather app, you won’t need to bother with it much.  But you’ll see it in the API so you should understand what it’s for.

Even in simple cases, it also gives you a chance to make things strongly typed. 

If you look at the base LoadContext:

   public class LoadContext {
 
        public object Identity { get; }
        public string UniqueKey {get;}
        public LoadContext(object identity) {}       
        
        // .. other stuff
    }

you’ll see the Identity property there is of type object.  So in this case, we’re just going to derive and give ourselves a nice strongly-typed property and constructor:

      public class ZipCodeLoadContext : LoadContext {
        public string ZipCode {
            get {
                return (string)Identity;
            }            
        }
 
        public ZipCodeLoadContext(string zipcode)
            : base(zipcode) {
 
        }
    }

One important thing to note here is that the parameter on the constructor should match the type of object you’ll be passing in.  In this case, the zip is going to be a string, so there should be a 1-parameter ctor that takes a string. AgFx will look for this to automatically create the LoadContext.

We can reuse this LoadContext for the zip code service as well.  Handy!

Step 3: Define your ViewModel object (or model, if you want to do it that way…)

Now that we’ve got the LoadContext, we can create our ViewModel.  ViewModels should derive from ModelItemBase<T>, which you’ll want to be familiar with.  So let’s look at some of the members  you’ll be using:

  • ModelItemBase.LastUpdated – this property tells you the last time an object’s data was fetched.  It will be the same even after the data is loaded from the cache, it only updates when new data is loaded.  This property is data-bindable, so you can bind it to a field in your UI to show the user when the data for this particular object was last loaded.
  • ModelItemBase.IsUpdating -  This property is also data-bindable, and flips to true whenever AgFx is loading new data for your object.  It doesn’t flip to true for cache loads (those should be fast!), just when it’s loading new data, usually from the network.  You can bind this to an “Updating” UI item that you want to be enabled (like a progress bar) or visible only when an update is occuring.
  • ModelItemBase.Refresh() – This method allows you to refresh any loaded item with new data, at any time before or after it’s expiration date.  You can call this in the click handler for a link or a button to cause a data refresh to happen (which, will cause IsUpdating to be true, and then LastUpdated to be changed as part of that process).

ModelItemBase derives from NotifyPropertyChangedBase, which is a default implementation of INotifyPropertyChanged and also adds some helpful functions:

  • NotifyPropertyChangedBase.RaisePropertyChanged – helper for firing INotifyPropertyChanged.PropertyChanged
  • NotifyPropertyChangedBase.AddDependentProperty – if you have properties that are dependent on one another, it can be annoying to have to notify property changes from a bunch of different places.  This allows you, in the object’s ctor, to declare properties that are dependent on each other.  So imagine your ViewModel has a Price property and a Discount property.  The price is dependent on the discount, so if Discount changes, then Price will have new value.  You want any UI bound to Price to automatically change, so you’d just do this:  AddDependentProperty(“Discount”, “Price”), which says that when Discount changes, fire a PropertyChanged notification for Price.
  • NotifyPropertyChangedBase.NotificationContext – if you have done any work off of the UI thread, you know that you can’t change the properties of UI objects from other threads.  Likewise, if you make property changes to an object that is databound to UI, then you also can’t fire change notifications off thread.  This property allows the thread switch to happen automatically.  Just initialize this property with the SynchronizationContext from the desired thread (typically you just pass DispatcherSynchronizationContext.Current) and you’re done.

Okay, that’s it for ModelItemBase.  Let’s create our ViewModel for the weather forecast:

First, let’s create the constructors:

 public class WeatherForecastVm : ModelItemBase<ZipCodeLoadContext>   {
  public WeatherForecastVm()  {  }
        
 public WeatherForecastVm(string zipcode): 
        base(new ZipCodeLoadContext(zipcode))   {     }

//...
}

This is pretty simple, but it’s important to note two things:

  1. You must have a default constructor
  2. You should have a constructor that takes your identifier and creates a LoadContext for it.  It’s not critical, but you’ll find it helpful.

Okay, now we add some properties to the ViewModel.

Most properties look like this:

 private string _city;
public string City
{
    get
    {
        return _city;
    }
    set
    {
        if (_city != value)
        {
            _city = value;
            RaisePropertyChanged("City");
        }
    }
}

Note the “RaisePropertyChanged” call at the bottom, which calls down to NotifyPropertyChangedBase declared above.  This let’s the UI know that it should update this value.

For collection properties, things are just a little bit different.   When you databind to collection properties, you typically use an ObservableCollection<T> so that any databound UI will automatically update when you make changes to your items.  That still works fine here, too, but with a small tweak

For collection properties in your ViewModel, that you should do the following:

  1. Make your collection property read/write.  These properties are often just read only but for AgFx it’s much simpler if they are read write.
  2. Don’t replace the value of the collection in the setter, clear your collection, then copy the items over from the new value.

The reason for this is because AgFx knows which instance of your object is bound to your UI, and always updates that instance (it uses the same instance across your application).  So when an update happens, it “copies” the property values from the fresh instance into the one currently held by the system.  Rather than doing this automatically, because you may want to manage how the copying happens, you’ll typically just do this:

 private ObservableCollection<WeatherPeriod> _wp = new ObservableCollection<WeatherPeriod>();
public ObservableCollection<WeatherPeriod> WeatherPeriods {
    get {
        return _wp;
    }
    public set {
         if (value == null) throw new ArgumentNullException();
        if (_wp != null) {
            _wp.Clear();

            foreach (var wp in value) {
               _wp.Add(wp);
            }
            
        }
       
        RaisePropertyChanged("WeatherPeriods");
    }
}

So now you can add all of your properties to fill out the shape of your ViewModel.

Step 4: Define your caching policy

When you fetch data from your service, how long is it good for? AgFx defines a very simple way to define how long data should be cached for in your object.

The way this is done is with the CachePolicyAttribute.   The constructor for CachePolicyAttribute takes two parameters:

  1. The CachePolicy itself.  CachePolicy can be one of the following values
    1. CachePolicy.NoCache – this object should not be cached.  Each time an application asks for this object, fetch an updated value.  This won’t be used very often, but hey, it could happen.
    2. CachePolicy.ValidCacheOnly – in this case, the data will only be returned from AgFx if it’s valid.  In other words, AgFx will check the cache, and return that value if it’s within it’s expiration time.  If not, new data will be fetched and returned when it is available.  In our sample here, the Weather data is set as ValidCacheOnly.  No use in showing weather data from last week!
    3. CachePolicy.CacheThenRefresh – this is likely the most commonly used policy. What this means is that AgFx should go ahead look for a cached value, and if it finds one, regardless of it’s expiration date it should return that value to the caller. At that point, if the data is expired, automatically kick off a refresh. Imagine a Facebook application. When the user starts the app, imagine the app shows the user’s News Feed. The app could either always just show a blank News Feed with “Updating…” and then go get new data (this would be the ValidCacheOnly behavior). Or it could show the feed in the last state that it was in, and then update that with any new items since the last check. Most apps chose the latter, and this setting does exactly that. If you were to be databound to a “NewsVeedVm”, the feed would load from the cache then automatically update when new data was available. No code necessary, AgFx does this for you.
    4. CachePolicy.AutoRefresh – This goes one step further and automatically kicks off a refresh of the data when the data reaches the end of it’s designated lifespan.
  2. A cache time (in seconds).   This defines how long your data should be cached for.  For something like weather data, this might be 15 minutes (60 * 15), and for ZipCode data it’s basically forever, so you can just put a really long cache period in there.

So we just apply this to our ViewModels as in the following:

 [CachePolicy(CachePolicy.ValidCacheOnly, 60 * 15)] 
    public class WeatherForecastVm : ModelItemBase<ZipCodeLoadContext>{…}

// this basically never changes, but we'll say 
// it's valid for a year.
[CachePolicy(CachePolicy.CacheThenRefresh, 3600 * 24 * 365)] 
    public class ZipCodeVm : ModelItemBase<ZipCodeLoadContext>{…}

That’s it.

Step 5: Defining your DataLoader

Okay, now is when the real party starts.   The DataLoader is the beating heart of your ViewModel objects. 

The DataLoader has two primary jobs:

  1. Configure a request for data to populate the object
  2. Take the raw data and create a model or ViewModel object out of it

Fortunately both of these are pretty easy.

Your DataLoader is an object that implements IDataLoader<T> where T is your LoadContext type from earlier.  Your DataLoader should be a public nested type in your view model.  This isn’t strictly necessary (see AgFx.DataLoaderAttribute) but I haven’t yet found a case where I need to do it otherwise.  If it’s a nested type, AgFx will automatically find and instantiate it.

IDataLoader<T> It has two methods:

  1. IDataLoader.GetLoadRequest – this returns an object that describes how to fetch data for your ViewModel.  In the vast majority of cases this will just be a URI, and there is a special class called WebLoadRequest for that.  GetLoadRequest actually returns an object of the abstract type LoadRequest, so you can do fancy stuff here if you need to.  At the end of the day, this takes information from your LoadContext (basically whatever you need to build the URI in most cases) and then tells AgFx how to convert this into a Stream.  Usually this means just doing a web request and returning the ResponseStream.
  2. IDataLoader.Deserialize – given the Stream from (1) above, Deserialize is responsible for turning that into an object of the type that AgFx is asking for (in other words, your view Model type).

Implementing these is also simple.  Here’s how GetLoadRequest usually looks:

 private const string ZipCodeUriFormat = "https://www.webservicex.net/uszip.asmx/GetInfoByZIP?USZip={0}";

public LoadRequest GetLoadRequest(ZipCodeLoadContext loadContext, Type objectType)
{
    // build the URI, return a WebLoadRequest.
    string uri = String.Format(ZipCodeUriFormat, loadContext.ZipCode);
    return new WebLoadRequest(loadContext, new Uri(uri));
}

Make sense?  It’s pretty simple.  If you needed more parameters on the request, then you’d add more properties to your LoadContext and then use that to build out the URI.

AgFx will take that object and it will result in a network request.  When that network request returns, the stream that it provided will be passed along back to the DataLoader for processing.

Processing the network data is typically straight-forward.  I’m doing manual XML walking here.  If your web service exposes a WSDL that the Windows Phone Developer Tools can handle, you can use that to make parsing a snap, usually using DataContractSerializer,, but for clarity I’ll just do the brute force method below.

 public object Deserialize(ZipCodeLoadContext loadContext, Type objectType, System.IO.Stream stream)
{

    // the XML will look like the following, so we parse it.

    //<?xml version="1.0" encoding="utf-8"?>
    //<NewDataSet>
    //  <Table>
    //    <CITY>Kirkland</CITY>
    //    <STATE>WA</STATE>
    //    <ZIP>98033</ZIP>
    //    <AREA_CODE>425</AREA_CODE>
    //    <TIME_ZONE>P</TIME_ZONE>
    //  </Table>
    //</NewDataSet>                

    var xml = XElement.Load(stream);


    var table = (
                from t in xml.Elements("Table")
                select t).FirstOrDefault();

    if (table == null) {
        throw new ArgumentException("Unknown zipcode " + loadContext.ZipCode);
    }

    ZipCodeVm vm = new ZipCodeVm(loadContext.ZipCode);
    vm.City = table.Element("CITY").Value;
    vm.State = table.Element("STATE").Value;
    vm.AreaCode = table.Element("AREA_CODE").Value;
    vm.TimeZone = table.Element("TIME_ZONE").Value;
    return vm;
}

So what we do here is get into the XML stream with XLINQ, then just create a new ViewModel object (of type ZipCodeVm) and populate it’s properties with the values that we got out of XML. 

Note the objectType property.  The returned object MUST be an instance of this type (derived types are OK).  AgFx will enforce this.  You can take a look at the Deserialize method in the WeatherForecastVm as well, it’s pretty much the same, but it handles much more complex XML from the NOAA service via a helper class.

Now we just compile to make sure it’s all building.

Step 6: Bind to your UI with Expression Blend

Believe it or not, our application is just about done.  Using the great sample data features of Expression Blend, it’s really easy to create our UI.

We’ll walk through a simple case of just the zip code information that we deserialized above.  To make this easy to understand, what we want to do is create a UserControl that shows the city and state, along with the LastUpdated date.  For the whole weather application, it’s the same process with a little more detail.

For this, I add a new Windows Phone User Control to my project, right click on the XAML file in Solution Explorer and choose “Open in Expression Blend…”.

Once Blend opens with the UserControl showing on the design surface, the first thing we’ll do is create sample data for our ViewModel:

image

Now we choose “Create Sample Data From Class”:

image

This will show us the public types in our project assemblies.  We’re looking for “ZipCodeVm”, which is one of the VM objects we created up in step 3.

image

Hit OK, and now just drag the ZipCodeVm item onto the design surface:

image

Now we just build out our UI like so by dragging on some TextBlock objects from the Blend toolbar:

image

Now, for each of the items that says “TextBlock” we can just databind to our data fields by dragging them onto the object on the design surface, as demonstrated by the red arrow below.  We’ll see sample (bogus) data that lets us just visualize how the UI will look at runtime.

image

Remember, it’s bogus sample data., it just allows us to see the layout.  This prevents the tweak-F5-tweak-F5 method that many of use are used to.

After a little cleanup, here is what the XAML looks like.   Notice the Binding statements to the properties we added in our ViewModel.

 <StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding City}" FontWeight="Bold"/>
        <TextBlock Text=", "/>
        <TextBlock Text="{Binding State}" />
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Updated: " />
        <TextBlock Text="{Binding LastUpdated}" />
    </StackPanel>
</StackPanel>

Step 7: Load your object data into your UI

Okay now we’ve got our View Models built, our data processing done, and our databinding done.  Now we just have to load the data.

The primary object for data access and management in AgFx is DataManagerDataManager is a singleton class that is accessed via the DataManager.Current property.

DataManager only exposes a few operations, but they really are key to tying the whole thing together.

As a starter example, we’d load data into the UserControl that we built above as follows:

 zipCodeUserControl1.DataContext =
          DataManager.Current.Load<ZipCodeVm>("98052");
  

Which gives us this at runtime:

image

Notice we are passing the zip code as a string.  Remember form our ZipCodeLoadContext,  that we added a constructor with just a single string parameter.  AgFx will take that “98052” and automatically pass it to that constructor.  Also notice that we don’t have to do anything to manage caching or networking here.  AgFx figures out if the value is already available in the cache, if the cache is valid, or if it needs to be refreshed from the network.

It’s important to keep in mind that the instance returned from the Load call will be the same instance that is returned from any future Load or Refresh calls.  This identity-tracking is one of the key features of AgFx.  What this means, is that once I’ve databound to a value in my UI, future calls to Refresh or Load will update this same instance. 

In other words, if in a button click you simply do this:

 private void btnRefreshZipcode_Click(object sender, RoutedEventArgs e) {
     DataManager.Current.Refresh<ZipCodeVm>("98052");
}

then the UI above will update.   Note we never touch the user control or it’s DataContext.  It just works.

DataManager gives you a broad set of operations to interact with your data. 

Remember these are all accessed via DataManager.Current:

  • DataManager.Load<> as noted above takes either the shortcut value or an actual LoadContext object that you construct yourself. You would construct a LoadContext if you needed to set additional parameters, like a page number or result set size.  Load executes asynchronously except in the case that there is already a value in memory, in which case that value is returned immediately.  If that value has expired, a reload is kicked off, and the current value is returned and will be updated when the load completes.
  • DataManager.Refresh<> , which reloads the data for the given item, as if the value has expired.  It otherwise works in the same way as Load<>.
  • DataManager.LoadFromCache<> works also similarly to Load<>, but it will only load from the cache and is synchronous.  This is generally used in app startup as a way to load things up before the UI kicks off, for a example credentials and whatnot.   You probably won’t use it often.
  • DataManager.Clear<> – clears the given object out of the DataManager’s  data structures.  Calling Load again will create a new instance, reload it’s live data, etc.
  • DataManager.RegisterProxy<> – this exists for some advanced scenarios that I won’t cover here, but basically this allows you to register other objects that you would like to be updated in addition to the main instance that DataManager is managing.
  • DataManager.UnhandledError (event) – this allows you to catch any unhandled errors that occur within a DataLoader’s code and potentially handle them before they bubble up to the app.
  • DataManager.IsLoading – this property is set to true whenever any object’s loader is currently fetching new data.   DataManager implements INotifyPropertyChanged, so you can bind this property to UI that runs a ProgressBar or something if you want to show UI whenever network activity is occurring.   This will not be set to true for cache loads, only for live loads (just like the ModelItemBase.IsUpdating property).

For Load and Refresh, there are overloads that allow you to get handle both completion and error scenarios in code.  The signatures look like this:

 public T Load<T>(object id, Action<T> completed, Action<Exception> error) ;

which allows you to pass in a lambda that will be called when the operation completes or fails, like so:

 this.DataContext = DataManager.Current.Load<WeatherForecastVm>(txtZipCode.Text,
        (vm) =>
        {
            // upon a succesful load, show the info panel.
            // this is a bit of a hack, but we can't databind against
            // a non-existant data context...
            info.Visibility = Visibility.Visible;
        },
        (ex) =>
        {
            MessageBox.Show("Error: " + ex.Message);
        });   

 

Note that if you pass in a handler for error, then the DataManager.UnhandledError event will not be invoked, regardless of what happens in the handler.  You either handle via the lambda, or the global event, not both.

The “vm” parameter in the success handler will be the same instance as the object returned from the Load<> function as well, so any operations on this object will also be reflected in the UI.  These handlers will invoke on the UI thread.

Finally, as I’ve mentioned before, it’s best to let DataManager do all the instance management for you.  It does a lot of caching so don’t cache instances yourself unless you have to.  For ViewModels that have properties that are other ViewModels (a pattern I really like because it encourages on-demand network activity as a user moves through an app), use this pattern:

 /// <summary>
/// ZipCode info is the name of the city for the zipcode.  This is a separate 
/// service lookup, so we treat it separately.
/// </summary>
public ZipCodeVm ZipCodeInfo
{
    get
    {                
        return DataManager.Current.Load<ZipCodeVm>(LoadContext.ZipCode);
    }
}

Just use the Load method to return the right value.

So back to our weather example, here is the parts of the UI that are serviced by the various objects we’ve been discussing:

image

Step 8: Debugging

If you build a debug version of AgFx, you’ll find some useful debugging output that will help you determine what AgFx is up to.  In the case of a first-time run of the weather application, here’s the output you’ll seee:

No cache found for NWSWeather.Sample.ViewModels.WeatherForecastVm (1)
3/17/2011 2:22:35 PM: Queuing load for WeatherForecastVm (2)
No cache found for NWSWeather.Sample.ViewModels.ZipCodeVm
3/17/2011 2:22:35 PM: Queuing load for ZipCodeVm
3/17/2011 2:22:35 PM: Checking cache for ZipCodeVm
3/17/2011 2:22:37 PM: Deserializing live data for NWSWeather.Sample.ViewModels.WeatherForecastVm (3)
Writing cache for WeatherForecastVm, IsOptimized=False, Will expire 3/17/2011 2:37:37 PM (4)
3/17/2011 2:23:43 PM: Deserializing live data for NWSWeather.Sample.ViewModels.ZipCodeVm
Writing cache for ZipCodeVm, IsOptimized=False, Will expire 3/16/2012 2:23:43 PM

Because these operations all happen off of the UI thread, you get interleaving between the call to the weather service and the zip code service.  That’s good!

I’ve added the numbers in (bold) to the right to describe that this output means:

  1. When the Load<WeatherForecastVm>(“98052”) call was made, the cache was checked and no value existed.
  2. Because no cache existed a load (network) was queued for this item
  3. The load completed and asked the DataLoader to deserialize the value
  4. The deserialize completed, so the stream is being written to the cache with the specified expiration date.

If you’re writing an app and it’s not doing what you think it should, this output will really help you understand what the problem might be.  If you see a lot of this output when you don’t expect to, that means something is going on that may be resulting in extra data loads that you don’t need.  So you can use this as a perf tuning input as well.

As a comparison, here’s the output from a second run, after the app has been exited and restarted a few minutes later.  Note there are no “queuing load” or “writing cache” entries.  This shows that cached data is actually being loaded, when it was last updated, and when it will expire.

3/17/2011 2:35:12 PM: Checking cache for WeatherForecastVm
3/17/2011 2:35:12 PM: Checking cache for ZipCodeVm
3/17/2011 2:35:12 PM: Loading cached data for ZipCodeVm, Last Updated=3/17/2011 2:23:43 PM, Expiration=3/16/2012 2:23:43 PM
3/17/2011 2:35:12 PM: Deserializing cached data for NWSWeather.Sample.ViewModels.ZipCodeVm, IsOptimized=False
3/17/2011 2:35:12 PM: Loading cached data for WeatherForecastVm, Last Updated=3/17/2011 2:22:37 PM, Expiration=3/17/2011 2:37:37 PM
3/17/2011 2:35:12 PM: Deserializing cached data for NWSWeather.Sample.ViewModels.WeatherForecastVm, IsOptimized=False

Wrap up

Phew, that’s a lot of stuff!  Believe it or not, there’s more but I’ll leave it at that for now.  I’ve had a lot of fun putting this framework together, with help from Jeff among others.  And I’ve had even more fun writing apps on top of it.  Hopefully you will too, please let me know how it goes!

Download AgFx with the NOAA Weather sample here.  Note when you open this project, you'll get a few warnings about Silverlight project.  These are expected and harmless because the base code is an Silverlight class library (so it can be used on Silverlight too).  Just hit OK.

Shawn