Architecting SL4 Apps with RIA Services, MEF and MVVM - Part 4 (of 3)


Url: http://wilderminds.blob.core.windows.net/downloads/riaxboxgames.zip

Silverlight Tour

Welcome the part 4 of my three-part series on architecting with RIA Services. In the last part of the series, I thought I was done with the example and some of my readers challenged me to help them understand how to handle Add/Delete scenarios. Since I was at it, I figured I should show paging and IsDirty scenarios as well, I decided to make a part four. 

Remember this example is based on my current thoughts, its not dogma. I will change my mind at times and learn from the community (as has even happened during this series).  Hopefully this example can help you think about how the patterns match your current business problems. No tithe, no sermon, no damnation....I promise ;)

Supporting IsDirty

To be able to support some functionality (like using it in some Commands' CanExecute call), I wanted to test the model for whether it had saveable entities.  My first try was to ask the RIA Services' context HasChanges property:

public bool HasChanges
{
  get { return Context.HasChanges; }
}

This works *but* the problem is that this property tests to see if the context has any changes.  Why does this matter?  I realized that as I made additional queries that the context was holding on to every object I retrieved. This means that if I changed an object and switched the Genre, the object was still in memory unchanged. More importantly, the size of the cache in the context was growing every time.  I didn't want this so I changed the way I was handling the returned values.

The context has a collection of EntitySet objects.  Each EntitySet is for a particular data type.  I wanted to change the behavior to only have a single set of games at any particular time. This way my call to HasChanges was only testing the current set of games, not every game I've ever loaded.  To do this, I made one small change:

void PerformGameQuery()
{
  // Clear the games so we don't consume a lot of memory
  Context.Games.Clear();

  ...
}

This allows us to remove games we no longer care about.

Server Paging

We should start with the paging story since that affects the design of the add/delete functionality. In RIA Services paging is pretty simple.  Since the query to the server can contain some very simple LINQ expressions, we can use the Skip and Take expressions to shape the result (Where and OrderBy are also supported). For example if we want to get the second page where the page size is ten records:

// Generate the query with paging
var qry = Context.GetGamesByGenreQuery("Shooters")
                .Skip(10)
                .Take(10);
Context.Load<T>(qry, OnGamesLoadedComplete);

The Skip expression tells RIA Services to look at the result (on the server) and skip those records; The Take expression tells RIA Services to limit the number of results returned (like a TOP clause in SQL).  Using them together allows us to page. But I like the hide the paging in the Model.  Sure the ViewModels will need to know about the paging, but they shouldn't control the size or method of paging. To achieve this, I've added a couple of properties to my model:

public class GamesModel : IGamesModel
{
  private int _currentGamesPage = 0;
  private readonly int GAMESPAGESIZE = 15; 
  private string _lastGenre;
  
  ...

Using these parts of the model, I can handle the size and current page. One problem with this design though is that its emparting state into the Model.  But since our Model already has the data context which holds onto references to returned elements, I think this is an acceptable solution.

The paging then becomes pretty simple, when GetGamesByGenre is called, we get the first page (and reset the paging mechanism):

public void GetGamesByGenreAsync(string genre)
{
  _currentGamesPage = 0;
  _lastGenre = genre;

  // Generate the query with paging
  var qry = Context.GetGamesByGenreQuery(_lastGenre)
                  .Skip(_currentGamesPage * GAMESPAGESIZE)
                  .Take(GAMESPAGESIZE);
  ... 

When the initial call to GetGamesByGenreAsync happens, we reset the paging and store the last genre (so we can use it when we get subsequent pages). In the query, we calculate the page and execute that query. When NextPage/PrevPage are called, we use this same data to calculate the subsequent pages:

public void GetPrevPageGamesAsync()
{
  if (_currentGamesPage > 0)
  {
    _currentGamesPage--;
    
    // Generate the query with paging
    var qry = Context.GetGamesByGenreQuery(_lastGenre)
                     .Skip(_currentGamesPage * GAMESPAGESIZE)
                     .Take(GAMESPAGESIZE);  
  }
}

public void GetNextPageGamesAsync()
{
  _currentGamesPage++;
  
  // Generate the query with paging
  var qry = Context.GetGamesByGenreQuery(_lastGenre)
                   .Skip(_currentGamesPage * GAMESPAGESIZE)
                   .Take(GAMESPAGESIZE);
}

Once the model supports this, the ViewModel can use it via a Command:

RelayCommand _prevPageCommand = null;

public RelayCommand PreviousPage 
{
  get
  {
    if (_prevPageCommand == null)
    {
      _prevPageCommand = new RelayCommand(
        () => _model.GetPrevPageGamesAsync(),
        () => Games != null && Games.Count() > 0);
    }
    return _prevPageCommand;
  }
}

private RelayCommand _nextPageCommand = null;

public RelayCommand NextPage
{
  get
  {
    if (_nextPageCommand == null)
    {
      _nextPageCommand = new RelayCommand(
        () => _model.GetNextPageGamesAsync(),
        () => Games != null && Games.Count() > 0);
    }
    return _nextPageCommand;
  }
}

Then these commands can be data bound to controls (in my case buttons). Typically I am immediately asked about how to know when you've reached the end of paging.  This is a little wierd because you could handle it by determining if the number returned is less than a full page but because it *can* be the last page and return the full number of the page you can't rely on it.  The other solution is to determine it on the server, but in most cases (as the server results can change in a Transactional System), I just try and get the next page and if the number of results equals zero, I move the page counter back one and reload the results.  Its not as efficent but it is only non-efficient in an edge case (returning exactly the page size as the last page.  In the Model I handle this like so:

else if (r.Entities.Count() == 0 && _currentGamesPage > 0)
{
  // If the page returned no results we
  // reached the end of a page edge
  // (important since we're not getting
  // full result counts from the server)
  GetPrevPageGamesAsync();
}

Dealing with Add/Delete

While I differ from the RIA teams view, I like to isolate the RIA Service layer inside the Model completely. That means that the model can be responsible for the actual adding removing of the items. But this represents a problem.  In our earlier parts to this series, I returned the results of the loading query directly to the ViewModel to use as it's data.  The datatype we returned was an IEnumerable<T> object.  Ordinarily this works but since we're supporting adding and removing, the IEnumerable<T> object doesn't support that. So I dug a little deeper to see if the actual returning object supported ICollection or ICollection<T>.  Nope.  In fact, the underlying object is a ReadOnlyCollection<T>. Yup, read-only.  So our original assumption to return the results wasn't helpful  Instead, I changed this to return the EntitySet directly from the RIA Services' Context object:

// Returning the raw Games collection since the
// entity results are a ReadOnly collection
evt(this, new EntityResultsArgs<Game>(Context.Games));

Since the underlying type that the EntityResultsHandler is expecting is still IEnumerable<T> we could do one of two things, either change the event to allow a richer object (e.g. ICollection) or let the model to be reponsible for Adding/Removing of objects.  I chose the latter:

public class GamesModel : IGamesModel
{
  ...
  
  public Game AddNewGame()
  {
    var g = new Game()
      {
        GameID = 0,
        Name = "*TODO*"
      };
    Context.Games.Add(g);
    return g;
  }

  public void DeleteGame(Game g)
  {
    Context.Games.Remove(g);
  }

  ...
}

I don't prefer this solution.  I would prefer to have a monitored ObservableCollection<T> that is watched for add/delete operations but since RIA Services doesn't work that way, this work around is acceptable.

To allow for any of the views to issue these commands, I chose to use another application message like we dicussed in prior parts of this series.  In this case, while the model is doing the real work of adding and removing, there is still some work to be done once the game is added or deleted.  For example here is the event for adding an item:

AppMessages.AddNewGameMessage.Register(this,
  ignore =>
  {
    CurrentGame = _model.AddNewGame();
  });

In the MVVM Light implementation of their Messenger, they don't allow for no actual data being sent with the message, so I just use an object and in the lambda I named it ignore so you could tell that it was a dummy piece of data. Inside the message handler, you can see I am calling the model to add the new game, but then setting the current game to be the new game (so that it can be immediately edited).  All of this happens in the GamesListViewModel so by setting the new game as the CurrentGame we get both that the ListBox in the view is selected as well as a message is being sent out (that the GameEditViewModel listens for) that a new CurrentGame has been selected which allows it to be edited.  Again, the model is responsible for the record-keeping, but the view model continues to work as expected to communicate with the actual view.

The delete message is the same:

AppMessages.DeleteCurrentGameMessage.Register(this,
  ignore => 
    {
      _model.DeleteGame(CurrentGame);
      CurrentGame = null;
    });

In this case we tell the model to delete the game and mark the current game as a null reference which prevents the editing in the edit view as well as deselects the item from the ListBox in the List view.  This pattern should allow you to continue separating your concerns between the different layers.

You can get the final version of the example here:

http://wilderminds.blob.core.windows.net/downloads/riaxboxgames.zip




Application Name WilderBlog Environment Name Production
Application Ver 1.1.0.0 Runtime Framework .NETCoreApp,Version=v1.1
App Path D:\home\site\wwwroot Runtime Version .NET Core 4.6.25009.03
Operating System Microsoft Windows 6.2.9200 Runtime Arch X86