Views: | 15689 |
Number of votes: | 5 |
Average rating: |
This article is number four in a series of articles about development of EPiServer Community sites. The EPiServer Community framework provides a wide variety of modules suitable for intranets such as webmail and document archives to modules suitable for modern online communities such as video galleries, chat and blogs.
Below the rest of the articles are listed, as well as the source code for the objects described.
» Creating a Custom EPiSever Community Module - Part One
» Creating a Custom EPiSever Community Module - Part Two
» Creating a Custom EPiSever Community Module - Part Three
» Download the full source code for this article.
We now have a fully functional module with which we can create, save, update and remove Movie objects. We still have a way to go to satisfy the requirements though as we have so far ignored the method for getting movies submitted by a specific user. Let’s begin by creating the stored procedure.
The stored procedure should return the movies created by a specified author. No sort order was specified in the requirements so we’ll assume that the movies should be sorted alphabetically by title. The stored procedure should also support paging. Here’s how we implement it:
CREATE PROCEDURE spGetMoviesByAuthor
@intAuthorID int,
@intPage int,
@intPageSize int
AS
BEGIN DECLARE @intStartRow int;
DECLARE @intEndRow int;
SET @intStartRow = (@intPage -1) * @intPageSize + 1;
SET @intEndRow = @intPage * @intPageSize; WITH movies AS
(SELECT intID, strTitle, intRuntime, intAuthorID,
ROW_NUMBER() OVER(ORDER BY strTitle DESC) as intRow,
COUNT(intID) OVER() AS intTotalItems
FROM tblMovie
WHERE intAuthorID = @intAuthorID)
SELECT intID, strTitle, intRuntime,
intAuthorID, intTotalItems
FROM movies
WHERE intRow BETWEEN @intStartRow AND @intEndRow END
We use common table expressions to first select all movies submitted by the author ordered by title. Each row in the set contains its row number relative to the set. We then select the rows for the specified page given the specified page size.
So far we haven’t created a collection class for the Movie type. In many situations the generic List class, or some other built in collection class, would do fine but as we would like cached collections of our entity types to be read only we’ll need to create a special collection class, MovieCollection. Luckily this is as easy as creating a new class and inheriting from CacheableCollectionBase<Movie, MovieCollection>:
using EPiServer.Common.Cache; namespace ExampleProjects.Movie
{
public class MovieCollection :
CacheableCollectionBase<Movie, MovieCollection>
{
}
}
The factory method, GetMoviesByAuthor, should be implemented like this:
internal static PagedCollectionResultSet GetMoviesByAuthor(
IUser author, int page, int pageSize)
{
object[] spParameters = new object[3];
spParameters[0] = author.ID;
spParameters[1] = page;
spParameters[2] = pageSize; int totalItems = 0;
MovieCollection movies = new MovieCollection();
using (DbDataReader reader =
DatabaseHandler.GetReader(
"spGetMoviesByAuthor", spParameters))
{
while (reader.Read())
{
if(totalItems == 0)
{
int ordinal =
reader.GetOrdinal("intTotalItems");
totalItems = reader.GetInt32(ordinal);
}
movies.Add(ConstructMovie(reader));
}
} PagedCollectionResultSet pagedMovies =
new PagedCollectionResultSet(movies, totalItems); return pagedMovies;
}
The method first creates an array of parameters for the stored procedure. It then creates a variable for holding the total number of movies that the author has created and a new MovieCollection for storing the movies in the specified page that the stored procedure returns. Then it executes the stored procedure and populates the two recently mentioned variables. Finally it creates a new PagedCollectionResultSet based on the two populated variables and returns it.
A PagedCollectionResultSet is basically a container class for a collection that contains a subset of entities and an integer that specifies the total number of entities in the set. The reason we are using it is that the handler method that, via the CacheHandler, will invoke this method will need both the MovieCollection and the totalItems variable while this method can only return a single object.
Just as with the GetMovie handler method the first step in creating the GetMoviesByAuthor method is creating a delegate and a delegate variable with the factory method as value:
internal delegate PagedCollectionResultSet GetMoviesByAuthorDelegate(
IUser author, int page, int pageSize); internal static GetMoviesByAuthorDelegate
_getMoviesByAuthorDelegate = Data.MovieFactory.GetMoviesByAuthor;
We can then proceed to create the GetMoviesByAuthor method:
public static MovieCollection GetMoviesByAuthor(
IUser author, int page, int pageSize, out int totalItems)
{
EntityValidator.ValidateIsCommittedEntity(author, "author"); object[] getterArgs = new object[3];
getterArgs[0] = author;
getterArgs[1] = page;
getterArgs[2] = pageSize; string[] baseCacheKey = new string[3];
baseCacheKey = GetMoviesByAuthorBaseCacheKey(author.ID); string[] cacheKey = new string[5];
cacheKey[0] = baseCacheKey[0];
cacheKey[1] = baseCacheKey[1];
cacheKey[2] = baseCacheKey[2];
cacheKey[3] = pageSize.ToString();
cacheKey[4] = page.ToString(); PagedCollectionResultSet pagedMovies =
(PagedCollectionResultSet)CacheHandler.GetCachedObject(
_getMoviesByAuthorDelegate, getterArgs, cacheKey); totalItems = pagedMovies.TotalItems; return (MovieCollection) pagedMovies.Collection;
} private static string[]
GetMoviesByAuthorBaseCacheKey(int authorID)
{
string[] cacheKey = new string[3];
cacheKey[0] = MovieModule.Instance.UniqueName;
cacheKey[1] = "GetMoviesByAuthor";
cacheKey[2] = authorID.ToString(); return cacheKey;
}
The method implementation is probably quite familiar by now. We begin by validating that the specified author is a persisted/committed entity. Then we create an array of arguments for the factory method and a cache key. With those and our GetMoviesByAuthorDelegate we then invoke the CacheHandlers GetCachedObject method which returns a PagedCollectionResultSet. We assign the value of the PagedCollectionResultSets TotalItems property to the totalItems out variable and finally return the MovieCollection from the PagedCollectionResultSet.
As you probably have noticed building the cache key is a bit different in that we are first getting the first three strings in the key from a method named GetMoviesByAuthorCacheKey. The reason for doing this is that we want to be able to clear the cache for all results of this method for a specific author. You’ll soon see what I mean.
Note that the above implementation relies on the GetMoviesByAuthorBaseCacheKey to return a string array with three strings in it. This is just for the sake of keeping this example short. In a real project we should make the cacheKey arrays length vary with the baseCacheKeys length.
Whenever a new movie that belongs in a cached collection is added or a movie that is in a cached collection is updated or removed we want to remove that collection from the cache. When dealing with paged results as we are in this example we also want to remove all other collections that are subsets of the same set from the cache.
This means that if a user adds another movie we want to clear all cached results of GetMoviesByAuthor for that user. The same goes for when a user updates or removes a movie. We must therefore modify the AddMovie, UpdateMovie and RemoveMovie methods in MovieHandler so that they clear the GetMoviesByAuthor cache for the movies author.
Let’s begin by creating a helper method:
private static void ClearGetMoviesByAuthorCache(int authorID)
{
string[] cacheKey =
GetMoviesByAuthorBaseCacheKey(authorID);
CacheHandler.RemoveCachedObject(cacheKey);
}
We then modify the AddMovie, UpdateMovie and RemoveMovie methods to invoke it:
public static Movie AddMovie(Movie movie)
{
. . . ClearGetMoviesByAuthorCache(movie.Author.ID); return addedMovie;
} public static void UpdateMovie(Movie movie)
{
. . . ClearGetMoviesByAuthorCache(movie.Author.ID);
} public static void RemoveMovie(Movie movie)
{
. . . ClearGetMoviesByAuthorCache(movie.Author.ID);
}
The code below illustrates a few ways the module that we have just built can be used:
//Create and add a new movie
Movie theGodfatherMovie = new Movie(
"The Godfather", CurrentUser);
theGodfatherMovie = MovieHandler.AddMovie(
theGodfatherMovie); //Create and add another new movie,
//this time with runtime
Movie gardenStateMovie = new Movie(
"Garden State", CurrentUser);
gardenStateMovie.RuntimeMinutes = 102;
gardenStateMovie = MovieHandler.AddMovie(
gardenStateMovie); //Get all movies by the current user
int totalAuthoredMovies;
MovieCollection authoredMovies =
MovieHandler.GetMoviesByAuthor(
CurrentUser, 1, int.MaxValue - 1,
out totalAuthoredMovies); //Rate the movies
IRating rating = new Rating(
theGodfatherMovie, 5, new UserAuthor(CurrentUser));
RatingHandler.Rate(rating); rating = new Rating(
gardenStateMovie, 4, new UserAuthor(CurrentUser));
RatingHandler.Rate(rating); //Get toprated movie
int totalRatedMovies;
int minimumVotesRequired = 1;
int page = 1;
int pageSize = 10;
PopularRatableEntityCollection topMovies =
RatingHandler.GetMostPopularItems(
typeof (Movie), minimumVotesRequired,
page, pageSize, out totalRatedMovies);
nice example! just one question here...How to enable this reporting for this custom Movie type? Followed the steps in the Community SDK for Reporting but can't see the new entity in Community abuse report gadget. And there are entries in the [tblEPiServerCommonReport] for the new entity