共用方式為


Build great Windows Phone applications the easy way!

Introducing AgFx

One of the things we’ve spent a fair amount of time on is working with various application writers, helping them build great Windows Phone 7 applications.  Many of the top applications that you’ll find on Windows Phone 7 devices today spent some time in a debugger on my desktop, or Jeff’s, or another one of the folks around here.

In doing this process, we saw a lot of common trouble spots for developers looking to write Windows Phone 7 applications and as I started to think more about the problem.

I thought the same thing I always think: “hmmmm, how can I build a framework that will make these things easy for the developer so they can worry about other stuff!”

And so it was born, and I’m currently calling it AgFx.  Fortunately I’m a lot better at building frameworks than I am at naming things, so I’ll leave it at that.  But if you’re wondering what the “Ag” is about, “Ag” is the symbol for Silver, and this framework happens to work on desktop Silverlight as well, so you can use it to build the guts of applications that are shareable across phone and desktop.


AgFx is available via CodePlex here.

What does it do?

AgFx provides a set of base class helpers and a data management engine that allow you to write your View Models (or just models, if you’re so inclined) in a very simple and consistent way.  It contains some other helpful stuff too, but the data management engine is the heart of it, which I’ll introduce in this post.

Most applications do some variant of the following sequence:

  1. Fetch data off of Internet
  2. Process said data into some data structures
  3. Bind said structures to some UI
  4. Cache structures on disk
  5. Next time a request comes in (say after tombstoning), check the cache
    1. If cache is valid, goto 2
    2. If cache is not valid, goto 1
  6. Repeat

And while the above sounds simple, it turns out it’s not.  In fact, it’s a lot of work to get it right AND even when it works you have lots of opportunities to cause performance problems or other things that you’ll have to go out and figure out later. 

But fortunately the patterns are consistent enough that we can build an infrastructure to automate most of the above.  If you are thinking of writing an application that’s similar to the above pattern, this will make it MUCH easier.

When thinking about most data-connected applications, it turns out there are only two key pieces of information required from the developer.  Consider a stock quote from a web service.  In order to display that stock quote in an application, we need to know:

  1. How to go fetch the data.  In this hypothetical case, it’s the URL to some service:  https://www.contoso.com/services/stockquotes?symbol=msft
  2. How to process the data into an object that is consumable by my application.  Typically it means parsing JSON or XML  that comes back from the request.

Everything else can be managed by the system, off of the UI thread in most cases:

  1. Checking for cached data and/or requesting new data
  2. Processing/Parsing the data
  3. Creating objects from the data
  4. Caching the data back to disk
  5. Handling data updates (these must be on the UI thread)

And it turns out that this is exactly what AgFx does.  It manages all of the above so you don’t have to.

Eh…code please.

Okay, let’s use a concrete example.   The app that I’ll be including with the bits here is a simple app that goes against the NOAA XML web services for weather reports.  Basically they take a US zip code and return a weather forecast.

Oh, the joys of winter in Seattle….

Anyway, as mentioned above, we need two pieces of information from the developer:  how to go find the data, and how to deserlialize it.  AgFx handles the rest.  The vast majority of your code when writing with AgFx is building these view model objects and deserializing data into them.

Let’s start with some examples.  First, AgFx view models usually look something like this:

     [CachePolicy(CachePolicy.ValidCacheOnly, 60 * 15)] 
    public class WeatherForecastVm : ModelItemBase<ZipCodeLoadContext>
    {
        public WeatherForecastVm()
        {
        }
        
        public WeatherForecastVm(string zipcode): 
        base(new ZipCodeLoadContext(zipcode))
        {
        }
    
       //...properties, methods
     }

A few things to note there.  First is the CachePolicyAttribute at the top.  This tells the system how to handle the caching for this object type.   “60 * 15” is 15 minutes – meaning these values are valid for 15 minutes.  CachePolicy.ValidCacheOnly means that the system should only return cached values that are within that cache time window, otherwise, it should go fetch an updated version.

Now, you’ll notice that the above is a generic type, deriving from ModelItemBase<T> , and is referencing something called a “ZipCodeLoadContext”.  Any object that AgFx is handling needs to have a LoadContext which is essentially the identifier for an instance of an item, as well as the place where you set extra state needed for loading.  It will become clear why it’s called a LoadContext shortly.  But the identifier should be unique for that type of item.  On many services it might be the user id (for users) or the item id (for some data item).  In this case the identifier is a zip code because zip codes map 1:1 with weather forecasts.  Given a zip code, we’ll always get the right forecast data (we all know we might not get the right forecast!).

In this case, the ZipCodeLoadContext looks like the following:

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

        public ZipCodeLoadContext(string zipcode)
            : base(zipcode) {

        }
    }

Given that this is a simple case, you’re just wrapping the zipcode string, really.  The framework allows you to shortcut that if that’s the case, but I am including it here for completeness and so we get a nice strongly typed “ZipCode” property.

Now to the important part.  The final piece is the DataLoader which is what holds this all together.  Here’s the DataLoader for the WeatherForecastVm, as a nested class inside the WeatherForecastVm class itself:

 public class WeatherForecastVm : ModelItemBase<ZipCodeLoadContext>
    {
       // ... VM Body removed

        /// <summary>
        /// Our loader, which knows how to do two things:
        /// 1. Build the URI for requesting data for a given zipcode
        /// 2. Parse the return value from that URI
        /// </summary>
        public class WeatherForecastVmLoader : IDataLoader<ZipCodeLoadContext> 
        {
           
 const string NWS_Rest_Format = "https://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?zipCodeList={0}&format=12+hourly&startDate={1:yyyy-MM-dd}";                        /// <summary>            /// Build a LoadRequest that knows how to fetch new data for our object.            /// In this case, it's just a URL so we construct the URL and then pass it to the            /// default WebLoadRequest object, along with our LoadContext            /// </summary>            public LoadRequest GetLoadRequest(ZipCodeLoadContext lc, Type objectType)<br>            {                string uri = String.Format(NWS_Rest_Format, lc.ZipCode, DateTime.Now.Date);                return new WebLoadRequest(lc, new Uri(uri));<br>            }            /// <summary>            /// Once our LoadRequest has executed, we'll be handed back a stream containing the response from the             /// above URI, which we'll parse.            ///             /// Note this will execute in two cases:            /// 1. When we fetch fresh data from the Internet            /// 2. When we are deserializing cached data off the disk.   














      /// The operation is equivelent at this point.            /// </summary>            public object Deserialize(ZipCodeLoadContext lc,  
 Type objectType,  
 System.IO.Stream stream)<br>            {                // Parse the XML out of the stream.<br>                var locs = NWSParser.ParseWeatherXml( 
 new string[] { lc.ZipCode }, stream);                // make sure we got the right data<br>                var loc = locs.FirstOrDefault();                if (loc == null)<br>                {                    throw new FormatException("Didn't get any weather data.");<br>                }                // Create our VM.  Note this is the same type as our containing object<br>                var vm = new WeatherForecastVm(lc.ZipCode);                                // push in the weather periods                foreach (var wp in loc.WeatherPeriods)<br>                {<br>                    vm.WeatherPeriods.Add(wp);<br>                }                return vm;<br>            }<br>        } 














    }

Again, the loader does two things, loads the new value (GetLoadRequest) and then parses it (Deserialize).   Note we never have to write any serialization code for caching, just deserialize and AgFx does the rest.

Using your data

All of your view models will follow the same pattern above – you’ll define the type, define it’s LoadContext (if necessary), then define it’s DataLoader.  At that point, you’re pretty much done. 

The way that all of these objects (viewmodels or strict models) are accessed is the same in AgFx, and that’s with the DataManager.

So, what’s the code to use the data in this application?  It’s just this:

 private void btnAddZipCode_Click(object sender, RoutedEventArgs e)
{
  // Load up a new ViewModel based on the zip.
  // This will either fetch new data from the Internet, or load the cached data off disk
  // as appropriate.
  //
  this.DataContext = DataManager.Current.Load<WeatherForecastVm>(txtZipCode.Text); 
}

That’s really it.  The rest of the code is databindings in the XAML, and some other code to save the zip code so it automatically loads again the next time.

What we are doing here is asking the DataManager to load an object of type WeatherForecastVm, with the given zip code as the identifier.  The framework takes care of the rest.

So, again, what are the steps that happen for me automatically upon calling that one line of code?

  1. Look in the cache for data that a WeatherForecastVm can load, with the unique identifier of the specified zip code.
  2. If the data is there, check it’s “expiration date”, if it’s not expired, return the data.
  3. If it is expired, go get new data from the web, then save it to disk
  4. Deserialize data from (2) or (3)
  5. Create a WeatherForecastVm object and populate it from the deserialized data
  6. Return the WeatherForecastVm instance so it can be used for databinding.

Almost all of this happens off of the UI thread (basically everything up to step 6). 

Furthermore, the DataManager tracks instances, so the instance that’s returned from the Load call will *always* be the same (for the given identity value) for your entire application.  This means that as long as you use this Load call, and databind to that object, any future refreshes of that data will automatically be reflected in your UI, regardless of where it is in your application.  You don’t need to worry about any of this, it just works.  More on this below.  AgFx does the caching and fast lookup for you, so don’t hold references to these values if you don’t absolutely have to.

Loading On Demand

Using DataManager.Load<> allows the framework to control when and where items are loaded.  This also allows your app to do work only as it’s needed to populate your UI.  And once you break up the work into discreet view model objects, you can also control their caching policy independently.  If you look at the WeatherForecastVm in the sample, you’ll see the following property:

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

Note that this property results in a call to another VM class – “ZipCodeVm”.  If you look at the image up above, you’ll see that the name “Redmond” is shown under the 98052 zip code.  This information didn’t come down with the weather data, I had to fetch it from another service.  But since it’s based off the same identifier (“98052”) as the weather forecast, we just pass that along. 

When I want my UI to show the city, as above, I’ll databind a TextBlock like so:

 <TextBlock Text="{Binding ZipCodeInfo.City}"  FontWeight="Bold"/>

To which the databinding engine does the following steps:

  1. On the current DataContext, look for a property called “ZipCodeInfo”
  2. Fetch that value, and then (if not null) look for a property called “City”
  3. Fetch that value and set it as the Text property

So, back to our WeatherForecastVm object, when the databinding engine asks for the ZipCodeInfo property value,that request will be kicked off.  But here’s the trick.  It’s kicked off asynchronously.  Execution won’t be held up while that value is fetched.  So what does it return?

I mentioned above that the instance returned from then Load call will always been the same for a given instance*.  So here’s what happens:

  1. The Load<> call is made
  2. A default instance of ZipCodeVm is created and returned.  The UI will databind against this instance
  3. AgFx does the off-thread work of getting the value from the web service, deserializing it into a ZipCodeVm object, and caching it on disk.
  4. AgFx then takes and copies the updated values into the properties of the instance that was created.  Since it is this instance that is being databound against, the UI will automatically update with these new values.
  5. Any future Load calls, if new data is fetched, will also update this instance.

The net result is that UI anywhere in the application that is bound to a value retrieved via the Load<> method will always be kept up to date.  No code wiring needed.

Finally, another upside to breaking up object like this is that you can specify a different cache policy.  If you remember, the WeatherForecastVm policy was ValidOnly, for 15 minutes.

Zip codes don’t change much so we’re doing this instead:

 // 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>{...}

CacheThenRefresh means that if a request is made and there is a cached value that has expired, go ahead and return that cached value and automatically kick off a refresh.  Just as in the case above, the refresh will update the instance that was created out of the cache and your UI will update when the refresh happens.  A common case for this is an app that shows a Twitter feed.  When you launch Twitter, you may want to see the feed as it was last time you opened the app, then have the new posts show as they come in off the network.

* this isn’t strictly true – if no one is holding the value, it can be garbage collected so it’s not using memory, and then a new instance will be created on demand at the next request.  But since it’s not being held in memory, your code will never know that.

Take a look

That’s a quick introduction to AgFx, and there is a LOT more.  We’ve been writing apps internally on top of it for a while now and we’re having great success and there are a bunch of more features that allow you to handle things your app might need to do quickly and efficiently.  So for now, grab the code and run the app and get a feel for the framework.  Ask questions in the comments and I’ll start digging into more advanced features in posts I’ll do shortly.

AgFx is available via CodePlex here.

Comments

  • Anonymous
    March 14, 2011
    Looks very interesting, looking forward to trying it out. I do have one question though. Is there some way to retrieve whether loading of the data is in progress? Specifically I'd like to show a progressbar while the request is executing. I could just bind the visiblity of this progressbar to the count of the model with a converter (meaning the bar would show until there are no items returned), however in case of no results the progressbar will be there forever. Another question that springs into mind is how does AgFx notify of errors (e.g. network errors)? Thanks, Gergely

  • Anonymous
    March 15, 2011
    Hi Gergely - Yes, it does all of this and more, I was afraid of packing too much into the intro post.  First, take a look at the Weather sample in the zip file.  It has some examples of this stuff. But, specifically:

  1. Updating - yes you can get update notifications at a global level or at a per object level.  Globally, there is a property called DataManager.IsLoading that flips to true whenever an object "live load" is happening.  Cache loads don't flip this bit.  The DataManager.Current instance implements INotifyPropertyChanged so you can databind to this property.  Likewise, ModelItemBase also has an IsUpdating property, as well as a property called LastUpdated, which is a DateTime that tells you when the last time the data from an object was loaded, again "live load" only.  If you load cached data, you'll still get the last live load time.  In both of these cases, you can databind this property to a UI element's visibility property using the included VisibilityConverter: <TextBlock Text="Updating..." Visibility="{Binding IsUpdating, Converter={StaticResource VisibilityConverter1}}" /> To see if network loads are happening across the app, bind to DataManager.Current.IsLoading rather than ModelItemBase.IsUpdating. See NWSWeatherSample.MainPage.xaml for this usage, as well as the LastUpdated.
  2. Load/Error notification - yes, DataManager.Current.Load<> has an overload of the following signature: Load<T>(LoadContext loadContext, Action<T> success, Action<Exception> error) that allows you to get notification when the load completes, or if an error of any kind happens in the process. For example: DataManager.Current.Load<MyViewModel>("1234", null, (ex) => MessageBox.Show("Error: " + ex.ToString())); Finally, DataManager also has an UnhandledError event that gives you a chance to handle any errors not handled at the Load<> call site. Hope that helps.  Let me know if you have more questions. Thanks, Shawn
  • Anonymous
    March 15, 2011
    I am wanting to write an app, in version 1 to use Isolated Storage only.  Then in version 2, to use a web service to share the data across multiple devices.  this is a simple "make a list" kind of app. what would the IDataLoader implementation look like?  for example, the first time the app loads, I start with a ViewModel instance which has an empty list of items.  main page is list of items, and a page for editing an item.

  • Anonymous
    March 16, 2011
    Thanks, Shawn for the detailed answer. This should have me up and running! On a note - I'm surprised that this library hasn't gotten larger publicity so far considering that it seems to do all the essential things any data driven app needs. I'm sure that will change soon :)

  • Anonymous
    March 17, 2011
    Hi Shawn, I recently downloaded your project from msdn. Whenever I am trying to open your project in VS2010 Ultimate I am getting error "Adding a reference to silverlight project might not work properly. Even i ignore this error and try to run your project, I am not able to start debugging even.

  • Anonymous
    March 17, 2011
    @Gergely - yes, thanks, let's see what happens! @Jay - those are expected, and I made a clearer note above.  It shouldn't affect your debugging, but make sure you right-click on the NWS Sample and choose "Set as startup project".  The other projects are class libraries and can't be directly started. @Burton - If you're writing a "Make a list" app, I think AgFx already does what you need.  I assume the behavior you want is the following:

  1. User starts app
  2. App checks IsoStore for the list, displays that
  3. App checks Webservice for an updated list, displays that if necessary
  4. User changes items
  5. App pushes changes to web service Is this correct? If so, your data loader would look pretty much like the above: new WebLoadRequest(new Uri("www.mylistservice.com/getlist)); And then you'd deserialize the list as normal. The hitch is this.  There is no direct way to write items to the cache, they need to come through a loader.  So typically in your case what would happen is that you'd manually push the data to your service: www.mylistservice.com/additem Then you'd call refresh on your loader to turn around and get the updated value again: DataManager.Current.Refresh<ListVm>("4321"); This would then cache the value locally and update your UI.  It does seem like extra work since you might have the value locally.  HOWEVER, in your scenario it's critical, I think, that the app reflects the state of the service so you want to make sure it actually stuck.  Otherwise, you'll have users who open it on another device and are suprised their new item isn't there. You could do some more sophisticated queuing though to allow for adding items when not connected, etc. Hope that helps! For all of you, I just wrote a much more detailed post on the broader functionality.  Take a look!
  • Anonymous
    March 17, 2011
    .NET Time Period Library for .NET .Net Perf - timing profiler for .Net Weak Event Handlers Other Don