The oMovies Browser is a small project demonstrating how to interface with the Netflix oData catalog using Infragistics Silverlight controls. The oMovies project makes use of the xamGrid, xamSlider and Infragistics Virtual Collection class to facilitate paging.
In the browser you are able to navigate through the list of Netflix genres and view the associated titles in each genre:
You are also able to browse through the highest rated titles for a given year:
The solution consists of three projects. The web site is an ASP.NET MVC project, but this could have been any UI framework as the meat of the application lies in the Silverlight client.
Note: In reality you could collapse NetAdvNetFlix.Models in with the NetAdvNetFlix project, but in an effort to keep non-UI concerns from the interface layer of the application non-ui elements exist in a separate project.
Here is a screenshot of Solution Explorer with the everything expanded:
The first step required when working with the Netflix oData data catalog is to create a service reference to the oData feed. To do this, right-click on References in your project (in this case NetAdvNetFlix.Models) and select Add Service Reference…
This exposes a dialog that will allow you to make the connection to the feed. After adding http://odata.netflix.com/Catalog into the address bar, click Go and the dialog will display a representation of the data.
Click OK and now your project has a reference to the service location.
The oMovies browser uses the Genre and Title classes to model the data being requested from Netflix.
When interfacing with the Netflix service the data that is returned from the server is prepared for the application in order to make working with the data an easy process. Some data coming back is in the form of a collection of the Genre or Title classes. Other times the data coming back is the result of a request for a total count of a collection, where the response is a single integer value. To wrap up these responses, the oMovies application implements two custom event args classes: DataRequestEventArgs and SingleValueEventArgs.
DataRequestEventArgs is responsible for wrapping up paged collections of data that come back from the server. The listing below represents the class in its entirety:
using System;using System.Collections.Generic; public class DataRequestEventArgs<T> : EventArgs{ public IList<T> Data { get; set; } public int StartIndex { get; set; } public int TotalQueryCount { get; set; }}
Note: For in-depth details on the purpose and mechanics of using this type of class in a paged result set, please read Using the VirtualCollection for Server-Side xamWebGrid Paging.
SingleValueEventArgs is a class that is responsible for transporting responses from the server that are represented by a single value. The use in oMovies is to return the results for requests for counts of the total number of items in a collection of genres or titles.
public class SingleValueEventArgs<T> : System.EventArgs{ public T Value {get;private set;} public SingleValueEventArgs(T value) { this.Value = value; }}
These classes are used to wrap data coming from the server. The next section details the class responsible for communicating with the Netflix server directly.
The MovieCatalog class wraps up all the functionality of accessing the Netflix service, so view models are shielded from implementation details of data access. The primary responsibility of MovieCatalog is to:
The following sections will explain the code and discuss how it operates in context with the rest of the application. The class begins by bringing in the required using statements and declaring the events needed to support the behaviors of the class. Each event maps to one of the responsibilities outlined above.
using System;using System.Data.Services.Client;using System.IO;using System.Linq;using System.Net;using System.Windows.Browser;using NetAdvNetFlix.Models.NetFlixCatalog; public class MovieCatalog{ public event EventHandler<DataRequestEventArgs<Genre>> GenresLoaded; public event EventHandler<DataRequestEventArgs<Title>> TitlesLoaded; public event EventHandler<SingleValueEventArgs<int>> GenresTotalCountLoaded; public event EventHandler<SingleValueEventArgs<int>> GenreTitlesTotalCountLoaded;
Next, the private members are declared. The Netflix service uses DataServiceCollections to facilitate the interaction between the client and the server. DataServiceCollection is a generic class and is typed in this case with either the Genre or Title depending on the type of model being manipulated for the catalog.
private DataServiceCollection<Genre> _genres = null; private DataServiceCollection<Title> _titles = null; private int _genresStartIndex = 0; private string _movieType = "Movie";
Then the public properties required to administer paging data sets are declared in the class.
public int TitlesPageSize { get; set; } public int TitlesItemIndex { get; set; }
The constructor’s job is to create new instances of the declared DataServiceCollection classes and wire up the even handlers that run when data is returned from the server. The CreateTitlesCollection method wraps up the operation of instantiating a new DataServiceCollection because later in the class a fresh instance of _titles is required.
public void CreateTitlesCollection() { this._titles = new DataServiceCollection<Title>(); this._titles.LoadCompleted += new EventHandler<LoadCompletedEventArgs>(titles_LoadCompleted); } public MovieCatalog() { this._genres = new DataServiceCollection<Genre>(); this._genres.LoadCompleted += new EventHandler<LoadCompletedEventArgs>(genres_LoadCompleted); this.CreateTitlesCollection(); }
Next you encounter the first method that does something interesting! Before explaining the next method in detail, first consider what’s required from a client/server application in order to manage paging. When dealing with paged data sets there are three sets of values that are important: page size, item index and the resulting data collection.
The LoadGenresAsync method is responsible for returning a paged set list of Genres. Passing in the itemIndex and pageSize helps facilitate the paged sets. The start index is set aside in a member variable so the value is available to the call back method when the result is ready.
public void LoadGenresAsync(int itemIndex, int pageSize) { this._genres.Clear(); NetflixCatalog catalog = new NetflixCatalog(Config.NetFlixServiceLocation); var query = (from g in catalog.Genres orderby g.Name select g) .Skip(itemIndex) .Take(pageSize); this._genresStartIndex = itemIndex; _genres.LoadAsync(query); }
LoadCompleted is the callback method run after calling LoadGenresAsync. After checking to make sure the GenresLoaded event is being handled elsewhere in the application, the event args are prepared by passing the data collection and start index into the class and then raising the event.
private void genres_LoadCompleted(object sender, LoadCompletedEventArgs e) { if (this.GenresLoaded != null) { DataRequestEventArgs<Genre> args = new DataRequestEventArgs<Genre>(); args.Data = _genres; args.StartIndex = this._genresStartIndex; this.GenresLoaded(this, args); } }
As stated earlier in the series, requesting the count of a set of data is done by opening a WebClient request to the server based off the URL that returns the count for the query. In the GetGenresCountAsync method below, the URL to Netflix is used to return the total count of genres in the system. (The code in the actual project wraps this URL up in a Config class so that the URL string exists in only one place.)
public void GetGenresCountAsync() { WebClient client = new WebClient(); client.OpenReadCompleted += new OpenReadCompletedEventHandler(genresCount_OpenReadCompleted); client.OpenReadAsync(Config.NetFilxGenresCountLocation); }
Once the response is available for the genre count, the call back method uses a StreamReader to read in the result coming from the event args. This method uses some defensive coding techniques to limit the chance that an exception from a type conversion bubbles up the call stack. The fail safe is that if there is a problem, then a zero count is returned to the caller.
private void genresCount_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e) { int count = 0; try { Stream result = (Stream)e.Result; StreamReader rdr = new StreamReader(result); string countString = rdr.ReadToEnd(); int num; if (int.TryParse(countString, out num)) { count = num; } } catch (Exception) { // swallow exception during async call and return count of 0... // TODO: log error and move on } if (this.GenresTotalCountLoaded != null) { SingleValueEventArgs<int> args = new SingleValueEventArgs<int>(count); this.GenresTotalCountLoaded(this, args); } }
The next two methods implement the same behavior as the last two methods dealing with counts, except in this case the total number of titles associated with a given genre are counted instead. The URL template is injected with the given genre name in order to create the full request location. In the event that the genre name is more than one word, then the text must be URL encoded. The UrlEncode method will encode spaces as “+”, which will make the request fail, so the plus signs are replaced with the URL entity value for space, “%20”.
public void GetGenreTitlesTotalCountAsync(string genreTitle) { WebClient client = new WebClient(); string location = string.Format(Config.NetFlixTitlesCountLocationTemplate, HttpUtility.UrlEncode(genreTitle).Replace("+","%20")); Uri uri = new Uri(location); client.OpenReadCompleted += new OpenReadCompletedEventHandler(genreTitlesTotalCount_OpenReadCompleted); client.OpenReadAsync(uri); }
The callback method is nearly identical to the implementation for counting genres above.
private void genreTitlesTotalCount_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e) { int count = 0; try { Stream result = (Stream)e.Result; StreamReader rdr = new StreamReader(result); string countString = rdr.ReadToEnd(); int num; if (int.TryParse(countString, out num)) { count = num; } } catch (Exception) { // swallow exception during async call and return count of 0... // TODO: log error and move on } if (this.GenreTitlesTotalCountLoaded != null) { SingleValueEventArgs<int> args = new SingleValueEventArgs<int>(count); this.GenreTitlesTotalCountLoaded(this, args); } }
When the grid for displaying genres is presented the user, an individual genre is then selected and paged set of titles associated with that genre are returned to the client. LoadTitlesByGenreAsync is responsible for initiating the request to the server. Once the LINQ query is prepared it is sent to the LoadTitlesAsync method which is responsible for loading the DataServiceCollection. At this point, a fresh instance of the titles collection is required for the data to return correctly from the server.
public void LoadTitlesByGenreAsync(Genre genre, int itemIndex, int pageSize) { this.CreateTitlesCollection(); if (genre == null) throw new ArgumentException("An instance of the Genre class is required.", "genre"); this.TitlesItemIndex = itemIndex; this.TitlesPageSize = pageSize; NetflixCatalog catalog = new NetflixCatalog(Config.NetFlixServiceLocation); var query = (from g in catalog.Genres from t in g.Titles where g.Name == genre.Name select t) .Skip(itemIndex) .Take(pageSize); this.LoadTitlesAsync(query); }
In order to drive the screen that displays a given year’s highest rated movie the following method will build the appropriate query and pass it to LoadTitlesAsync as well.
public void LoadYearsHigestRatedMoviesAsync(int releaseYear) { this._titles.Clear(); NetflixCatalog catalog = new NetflixCatalog(Config.NetFlixServiceLocation); var query = (from t in catalog.Titles where t.AverageRating.Value > 4 && t.ReleaseYear == releaseYear && t.Type == _movieType orderby t.Name ascending select t); this.LoadTitlesAsync(query); }
LoadTitleByIdAsync attempts to load a single title based off the provided ID, but it must still use LoadTitlesAsync in order to process the result. The query result doesn’t know that you are expecting only a single item, so you will pull the distinct title from the response when it is processed.
public void LoadTitleByIdAsync(string id) { NetflixCatalog cataglog = new NetflixCatalog(Config.NetFlixServiceLocation); var query = (from t in cataglog.Titles where t.Id == id select t); this.LoadTitlesAsync(query); }
The implementation for LoadTitlesAsync is concise. In order to ensure the system doesn’t simply append new results to old query results, the first step is to clear the DataServiceCollection. Then LoadAsync is called to run the provided query on the server.
private void LoadTitlesAsync(IQueryable<Title> query) { this._titles.Clear(); this._titles.LoadAsync(query); }
Finally, the callback method for LoadAsync checks to see if the application is listening for the TitlesLoaded event, prepares the event args and raises the TitlesLoaded event.
private void titles_LoadCompleted(object sender, LoadCompletedEventArgs e) { if (this.TitlesLoaded != null) { DataRequestEventArgs<Title> args = new DataRequestEventArgs<Title>(); args.StartIndex = this.TitlesItemIndex; args.Data = this._titles; this.TitlesLoaded(this, args); } }}
You have now toured the MovieCatalog class and seen how to interface directly with the Netflix oData catalog. Using a class like this gives you a centralized place to house your data access code and relieves your view or view model from having to be aware of the back end service.
The ViewModelBase class is useful, yet simple. This class implements INotifyPropertyChanged so each of the concrete view models do not have to explicitly implement the interface. The implementation for RaisePropertyChangedEvent supports a param array of property names. This allows you to notify the application of changes on a single line if you are raising the event for multiple properties.
using System.ComponentModel;
public class ViewModelBase : INotifyPropertyChanged{ public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChangedEvent(params string[] propertyNames) { if (this.PropertyChanged != null) { foreach (string name in propertyNames) { this.PropertyChanged(this, new PropertyChangedEventArgs(name)); } } }}
During the lifecycle of the page, the VirtualCollection requires the total number of genres or titles so it can build provide enough information to the xamGrid to build a paging footer. At first blush you might think that using a standard LINQ statement like the following would be most appropriate:
int count = (from t in catalog.Titles where t.AverageRating.Value > 4 && t.ReleaseYear == DateTime.Today.Year && t.Type == "Movie" select t).Count();
Unfortunately the problem with this approach is the call to the server to return the count would be an asynchronous call. The code is not prepared in such a way to handle a call back, so you must fall back to another method of returning the counts from the server.
The Netflix feed exposes access to counts of just about any collection of data via the appropriate URL. For instance if you want to get the total number of genres in the Netflix database you could send and asynchronous request to this URL:
http://netflix.cloudapp.net/Catalog/Genres/$count
Alternatively if you wish to get the total number of titles associated with given genre, then this is the URL:
http://netflix.cloudapp.net/Catalog/Genres('{0}')/Titles/$count
Note: In the application, the token {0} is replaced with the selected genre using a call to string.Format. Also, each of these URLs are kept in a class named Config. Access to these URLs are restricted to properties that wrap up these strings keeping the URL definition in one place and one place only.
Now when you need to access a count, one of the above URLs is used in conjunction with a call to OpenReadAsync off the WebClient class in order to facilitate the asynchronous call to the data. The following illustrates how the oMovies application returns counts:
public event EventHandler<SingleValueEventArgs<int>> GenresTotalCountLoaded;
public void GetGenresCountAsync(){ WebClient client = new WebClient(); client.OpenReadCompleted += new OpenReadCompletedEventHandler(genresCount_OpenReadCompleted); client.OpenReadAsync(Config.NetFilxGenresCountLocation);} void genresCount_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e){ Stream result = (Stream)e.Result; StreamReader rdr = new StreamReader(result); int count = Convert.ToInt32(rdr.ReadToEnd()); if (this.GenresTotalCountLoaded != null) { SingleValueEventArgs<int> args = new SingleValueEventArgs<int>(count); this.GenresTotalCountLoaded(this, args); }}
The year’s highest rated page gives you a coverflow to browse through movies in order to select an individual title. This implementation uses the Silverlight Coverflow Control found on CodePlex. Some of the considerations for this page include:
Note: This page delays the call to get the selected movie information. Users may interact with the page by rapidly clicking through the coverflow images or using the slider to cycle through images before stopping on the final selection. In order to avoid unnecessary calls to the server for data that is never displayed, this page uses a DispatcherTimer to introduce a very short delay during the operation to fetch the individual title information.
Below is the code for the view model used to implement the year’s highest rated page. The first step is to bring in the required namespace used throughout the class and declare the class itself.
using System;using System.Collections.ObjectModel;using System.Collections.Generic;using System.Windows.Threading;using NetAdvNetFlix.Models;using NetAdvNetFlix.Models.NetFlixCatalog; public class YearsHighestRatedViewModel : ViewModelBase{
Next the private member variables are declared. This page is powered by using the selected year as the basis on which to query the server for a year’s worth of titles. The variable _selectedYear will hold this data throughout the lifecycle of the page and is initialized to the current year. The value for _minYear is arbitrary and may be set to a value of your liking. The _titles variable will hold the collection of titles returned from the server for the selected year. A busy indicator control in the XAML binds to the public counterparts to _busyText and _isLoading to notify the user that data is being loaded into the system. In order to validate the year values passed in by the users _tempYear is used to set aside the selected year. Lastly, _loadFromYearChange is a control flag required to help the routine responsible for loading Title data from the server know if the request is initiated from the user changing the date in scope.
private int _selectedYear = DateTime.Today.Year; private int _minYear = 1970; private Title _selectedTitle; private ObservableCollection<Title> _titles = new ObservableCollection<Title>(); private DispatcherTimer _timer; private string _busyText = "Loading..."; private bool _isLoading = false; private int _tempYear; private bool _loadFromYearChange = false;
The public properties are exposed to the view to allow the XAML page to data bind directly to the view model. The SelectedYear property implements validation logic to stop the system from accepting values outside the allowed range of years. If the SelectedYear ends up changing value, then _loadFromYearChange is flipped on and a request to load new titles based off the new year is initiated.
public int SelectedYear { get { return this._selectedYear; } set { this._tempYear = this._selectedYear; if (this._selectedYear != value) { this._selectedYear = value; if (this._selectedYear > DateTime.Today.Year) { this._selectedYear = DateTime.Today.Year; } if (this._selectedYear < this._minYear) { this._selectedYear = this._minYear; } this.RaisePropertyChangedEvent("SelectedYear"); if (this._tempYear != this._selectedYear) { this._loadFromYearChange = true; this.LoadTitles(); } } } }
As stated before IsLoading and BusyText exist to give clues to the user that data is being loaded on to the page.
public bool IsLoading { get { return this._isLoading; } set { if (this._isLoading != value) { this._isLoading = value; this.RaisePropertyChangedEvent("IsLoading"); } } } public string BusyText { get { return this._busyText; } set { if (this._busyText != value) { this._busyText = value; this.RaisePropertyChangedEvent("BusyText"); } } }
The Titles property exposes the loaded title data.
public ObservableCollection<Title> Titles { get { return this._titles; } private set { if (this._titles != value) { this._titles = value; this.RaisePropertyChangedEvent("Titles", "TitlesLastIndex"); } } }
TitlesLastIndex is required to feed to the xamNumericSlider. This value is used to set as the maximum value of the slider.
public int TitlesLastIndex { get { if (this._titles.Count > 0) { return this._titles.Count - 1; } else { return 0; } } }
The last public property is SelectedTitle. This property is exposed to allow the view to display title, description and box art information of the user’s selection.
public Title SelectedTitle { get { return this._selectedTitle; } private set { if (this._selectedTitle != value) { this._selectedTitle = value; this.RaisePropertyChangedEvent("SelectedTitle"); } } }
The constructor prepares a DispatcherTimer by assigning the event handler and initializing the interval to 700 milliseconds. This is just enough time to wait for additional input from the user if they are clicking rapidly through the coverflow or scrolling with the slider.
public YearsHighestRatedViewModel() { this._timer = new DispatcherTimer(); this._timer.Tick += new EventHandler(_timer_Tick); this._timer.Interval = new TimeSpan(0, 0, 0, 0, 700); }
The CreateCatalog method is used throughout the view model to provide a new instance of the MovieCatalog.
private MovieCatalog CreateCatalog() { MovieCatalog catalog = new MovieCatalog(); catalog.TitlesLoaded += new EventHandler<DataRequestEventArgs<Title>>(_movieCatalog_TitlesLoaded); return catalog; }
The next set of methods are utility methods that help notify the user that a request is in progress to the server. NotifyLoadStart handles flipping on the IsLoaded property and filling the BusyText property with a message. The two properties are bound to the busy indicator in XAML, so NotifyLoadStart/Stop make it easier to control these values in the view model.
private void NotifyLoadStart(string message) { if (!this.IsLoading) { this.IsLoading = true; this.BusyText = message; } } private void NotifyLoadStop() { this.IsLoading = false; }
LoadTitles is used by a few other methods to make the call to the server to fetch title data from the Netflix service.
public void LoadTitles() { this.NotifyLoadStart(string.Format("Loading titles from {0}...",this.SelectedYear)); MovieCatalog catalog = this.CreateCatalog(); catalog.LoadYearsHigestRatedMoviesAsync(this.SelectedYear); }
The callback method for is _movieCatalog_TitlesLoaded. This method processes the results of titles that return from the server – be that a collection of titles or a request for a single title. At the start of the method the event args are interrogated to see if there is data returned from the server. If a collection is present and the data is being loaded as a result of a change in the year (when the highest rated by year page is being viewed) or if there is no data in the Titles collection, then the response from the server is loaded into Titles, and the _loadFromYearChange is flipped off. Otherwise the _selectedTitle property is set to the result coming from the server.
private void _movieCatalog_TitlesLoaded(object sender, DataRequestEventArgs<Title> e) { if ((e.Data.Count >= 1 && this._loadFromYearChange) || this.Titles.Count ==0) { this._loadFromYearChange = false; this.Titles.Clear(); foreach (Title title in e.Data) { this.Titles.Add(title); } this.RaisePropertyChangedEvent("Titles", "TitlesLastIndex"); this.SelectedTitle = e.Data[0]; if (this.TitleCollectionLoaded != null) { this.TitleCollectionLoaded(this, EventArgs.Empty); } } else { this.SelectedTitle = e.Data[0]; } this.NotifyLoadStop(); }
When a user clicks on the cover art in the coverflow, the item is selected by populating the _selectedTitle variable. The data is not immediately requested from the server. The timer is reset to start the count down again and will tick if another selection isn’t made within 700 milliseconds.
public void LoadSelectedTitle(Title selectedTitle) { this._selectedTitle = selectedTitle; this._timer.Reset(); }
Look carefully at the code above. Notice that the DispatcherTimer class does not normally have a method named Reset. The code in this project uses an extension method added to the DispaterTimer class to rest the timeout. This enables the code to stop the timer if it is running and start the count down over gain, thus delaying the timeout until it's not reset again.
public static void Reset(this DispatcherTimer timer){ if (timer.IsEnabled) { timer.Stop(); } timer.Start();}
When the timer finally does timeout the Tick event is raised. The method below handles the Tick event and this is where the explicit call to the server is made to request title data from the server.
private void _timer_Tick(object sender, EventArgs e) { this._timer.Stop(); MovieCatalog catalog = this.CreateCatalog(); catalog.LoadTitleByIdAsync(this._selectedTitle.Id); }
Finally the last part of the view model are some helper classes called by the view to increment and decrement the selected year.
public void AddYear() { this.SelectedYear++; } public void SubtractYear() { this.SelectedYear--; }}
The Genres screen allows a user to browse through the 600+ genres and each of their associated titles. In this section you’ll see how the view model is implemented that supports the Genres view.
The class definition begins by declaring a number of private variables. The use of each variable is probably pretty obvious by their names, but each are discussed in context of it’s use.
public class GenreViewModel : ViewModelBase{ private VirtualCollection<Genre> _genres = null; private int _genresTotalCount = 0; private Genre _selectedGenre = null; private int _genresPageSize = 0; private VirtualCollection<Title> _titles = null; private int _titlesTotalCount = 0; private int _titlesPageSize = 20; private string _busyText = "Loading..."; private MovieCatalog _movieCatalog = null; private bool _isLoading = false;
There are a number of public properties required to expose to the view in order to display data correctly to the user. The page first loads with a list of Genres and then allow the user to select an individual genre to view it’s titles. While the user is interacting with the page, the IsLoading and BusyText properties are used to provide continual feedback to the user about what is happening in the background.
public VirtualCollection<Genre> Genres { get { return this._genres; } set { if (this._genres != value) { this._genres = value; this.RaisePropertyChangedEvent("Genres"); } } } public VirtualCollection<Title> Titles { get { return this._titles; } set { if (this._titles != value) { this._titles = value; this.RaisePropertyChangedEvent("Titles"); } } } public bool IsLoading { get { return this._isLoading; } set { if (this._isLoading != value) { this._isLoading = value; this.RaisePropertyChangedEvent("IsLoading"); } } } public string BusyText { get { return this._busyText; } set { if (this._busyText != value) { this._busyText = value; this.RaisePropertyChangedEvent("BusyText"); } } }
Just as the the YearsHighestRatedViewModel included some utility methods control the properties accessed by the view. The properties being manipulated in these methods are bound in the view to a BusyIndicator control to help keep the user informed what is happening in the background.
The constructor of the view model accepts two parameters to help provide basic information to the class. The page sizes of the Genre and Titles collections is required to work with the Infragistics Virtual Collection to enable server side paging for the view. A new instance of the data access class MovieCatalog is created and the requisite event handlers are wired up in the class to facilitate interaction with the server. After the event handlers are created then the first task of requesting the total number on Genres from the server is initiated.
Note: All calls to the server are conducted asynchronously so the request is only initiated here.
public GenreViewModel(int genrePageSize, int titlePageSize) { this._genresPageSize = genrePageSize; this._titlesPageSize = titlePageSize; this._movieCatalog = new MovieCatalog(); this._movieCatalog.GenresLoaded += new EventHandler<DataRequestEventArgs<Genre>>(_movieCatalog_GenresLoaded); this._movieCatalog.GenresTotalCountLoaded += new EventHandler<SingleValueEventArgs<int>>(_movieCatalog_GenresTotalCountLoaded); this._movieCatalog.TitlesLoaded += new EventHandler<DataRequestEventArgs<Title>>(_movieCatalog_TitlesLoaded); this._movieCatalog.GenreTitlesTotalCountLoaded += new EventHandler<SingleValueEventArgs<int>>(_movieCatalog_GenreTitlesTotalCountLoaded); this.NotifyLoadStart("Loading Genres Total Count..."); this._movieCatalog.GetGenresCountAsync(); }
In the previous section on the MovieCataglog class the process of returning genre counts is discussed. Here the view model is leveraging this functionality to get a call back from the server to set aside the total number of genres in the Netflix system. Once the count is cached in the view model then the Infragistics Virtual Collection is created to handle the sets of title data maintained throughout the user’s interaction on the page. The Virtual Collection is responsible for initiating requests to the server to return the appropriate sets of data based on the parameters set on the class.
private void _movieCatalog_GenresTotalCountLoaded(object sender, SingleValueEventArgs<int> e) { this._genresTotalCount = e.Value; this.NotifyLoadStop(); this._genres = new VirtualCollection<Genre>(this._genresTotalCount, this._genresPageSize); this._genres.PageSize = this._genresPageSize; this._genres.ItemDataRequested += new EventHandler<ItemDataRequestedEventArgs>(_genres_ItemDataRequested); this.RaisePropertyChangedEvent("Genres"); }
When the xamGrid on the Silverlight page begins to request data, the Virtual Collection’s ItemDataRequested event will fire and the following event handler is responsible for processing the request. The method below starts the request for the the current page’s genre data.
private void _genres_ItemDataRequested(object sender, ItemDataRequestedEventArgs e) { if (this._genresTotalCount > 0) { this.NotifyLoadStart("Loading Genres..."); this._movieCatalog.LoadGenresAsync(e.StartIndex, e.ItemsCount); } }
Once the collection of genre data is available from the server then the result is loaded into the Virtual Collection and the anticipated count gets it’s value from _genreTotalCount.
private void _movieCatalog_GenresLoaded(object sender, DataRequestEventArgs<Genre> e) { this._genres.LoadItems(e.StartIndex, e.Data); this._genres.AnticipatedCount = this._genresTotalCount; this.NotifyLoadStop(); }
When a user clicks on a genre title in the genre grid then a selection changed event handler is run and LoadTitles as noted below runs. The event args on the grid pass to the handler the selected item and that value is passed into the LoadTitles method below:
public void LoadTitles(Genre genre) { this._selectedGenre = genre; this.NotifyLoadStart(string.Format("Loading '{0}' titles...", this._selectedGenre.Name)); this._titlesTotalCount = 0; this.LoadTitles(); }
Once the selected genre is cached then the parameter-less version of the LoadTitles method is called. Here a new instance of the titles Virtual Collection is created for the selected genre and the ItemRequested event handler is wired up. Another version of the LoadTitles method is called this time requesting the first page by passing in a zero value for the start index and the assigned page size. If the there is no total count available for the titles, then the loading of the data is skipped momentarily and the count is loaded first. Then once the count is available then this method is run again and can load the titles from the server.
private void LoadTitles() { if (this._titlesTotalCount > 0) { this._titles = new VirtualCollection<Title>(this._titlesTotalCount, this._titlesPageSize); this._titles.PageSize = this._titlesPageSize; this._titles.ItemDataRequested += new EventHandler<ItemDataRequestedEventArgs>(_titles_ItemDataRequested); this.LoadTitles(0, this._titles.PageSize); } else { this._movieCatalog.GetGenreTitlesTotalCountAsync(this._selectedGenre.Name); } }
Finally the version of LoadTitles which accepts the start index and item count is what encapsulates the logic that initiates the real request to get the titles from the server.
private void LoadTitles(int startIndex, int itemsCount) { this.NotifyLoadStart(string.Format("Loading '{0}' titles...", this._selectedGenre.Name)); this._movieCatalog.LoadTitlesByGenreAsync(this._selectedGenre, startIndex, itemsCount); }
As the user pages through the titles grid, more titles are required to fill the grid pages. This event handler will fire when the Virtual Collection needs more data to display.
private void _titles_ItemDataRequested(object sender, ItemDataRequestedEventArgs e) { if (this._selectedGenre != null) { this.LoadTitles(e.StartIndex, e.ItemsCount); } }
The following method is the callback used to cache the total number of titles found associated with a genre. Once the count is available then the view model will attempt to load the titles from the server.
private void _movieCatalog_GenreTitlesTotalCountLoaded(object sender, SingleValueEventArgs<int> e) { this._titlesTotalCount = e.Value; this.LoadTitles(); }
Finally, when the titles are returned from the server then the data is loaded into the Virtual Collection and the view is notified of the recent changes to the view model’s state.
private void _movieCatalog_TitlesLoaded(object sender, DataRequestEventArgs<Title> e) { this._titles.LoadItems(e.StartIndex, e.Data); this._titles.AnticipatedCount = this._titlesTotalCount; this.RaisePropertyChangedEvent("Titles"); this.NotifyLoadStop(); }}
Now that both view models are fully implemented the view is wired up by simple XAML data binding to the controls. Make sure to download the code to see how the rest of the application is implemented.
If you’d like to dive ever further into the oMovies browser, you can download the code here.
I had the good fortune of working with Infragistics' own Martin Silva (@nartub) and Juan Pablo Brocca (@jpbrocca) on the visual design of oMovies.