Introduction
LaxCraft is an ASP.NET MVC3 site that allows a youth team to keep track of their effort on and off the field. It is currently designed just to support one team and is geared towards
Lacrosse (hence the name) but it could easily evolve to supporting multiple teams or even multiple leagues and sports. Instead of just retiring the site after our season was over,
I figured I would break the application down and highlight some of the cool features so others can learn from it because it may be a bit more exciting than the typical Twitter Feed
or Hello World application.
Background
The site leverages the technologies below:
- Castle Windsor for Dependency Injection (Ninject or another IoC product would work just fine)
- Fluent NHibernate, a nice layer that sits on top of NHibernate that allows for XML-less configuration and allows for true
DDD because it will build out your database tables for you
- Razor Views for presentation which I find a little cleaner than the standard ASP.NET view engine
- Telerik MVC UI Extensions to have a WYSIWYG editor when editing or creating news items
Using the code
The code with a backup of the database can be grabbed at http://laxcraft.codeplex.com/ because the code is too
large to house here at CodeProject.
You can restore the database if you have a SQL Server instance or you can have Fluent-NHibernate build out your database by changing the connection string
of the ApplicationServices
key in the web.config and ensuring SchemaUpdate
is set to "Build
".
General Design / Overview
Overview
I built and used LaxCraft to give the kids on my team some motivation to practice on their own. They will play XBox hours on end, but today, kids don't get outside and play
as much as we used to. The site mimics some of the video games kids play where they can earn XP and Level Up in certain categories. The site allows for a player to login
and then log any effort they have put towards a particular stat. A stat will translate that effort into XP gained and adjust their level for that stat accordingly.
The more XP a player earns in a particular stat, the slower they will Level. The application shows a Team View of all players and what level each player is for each
stat and a Player View that shows in detail where a player is for each stat with a detailed stat description and how much more XP they need to move on to the next level.
Domain Design
A user can belong to a Player or be an Administrator. A Player can enter effort for certain stats and get XP for it, while an Administrator can manage all stats across all players.
It is a fairly generic design to allow for the flexibility of working with any type of progress tracking, not just Lacrosse or Sports.
Points of Interest
Dependency Injection
The Global.asax.cs Application_Start
method will call the BootstrapContainer()
method that will setup which concrete classes will
be used for a particular interface. The Install
method is going to look for all classes that inherit from IWindsorInstaller
within the current
assembly and call their Install
method. I think I like the way Ninject does Dependency Injection a little better because I find it to be a little cleaner and easier to follow,
but Castle's Windsor is what I used for this project.
Global.asax.cs
private static void BootstrapContainer()
{
container = new WindsorContainer()
.Install(FromAssembly.This());
var controllerFactory = new WindsorControllerFactory(container.Kernel);
ControllerBuilder.Current.SetControllerFactory(controllerFactory);
}
PersistenceInstaller.cs
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.AddFacility<persistencefacility>();
}
PersistenceFacility
will tell the application how it will be storing data and tells Fluent what database it should be talking to for persistence.
PersistenceFacility.cs
protected override void Init()
{
var config = BuildDatabaseConfiguration();
Kernel.Register(
Component.For<isessionfactory>()
.UsingFactoryMethod(config.BuildSessionFactory),
Component.For<isession>()
.UsingFactoryMethod(k => k.Resolve<isessionfactory>().OpenSession())
.LifeStyle.PerWebRequest);
}
private Configuration BuildDatabaseConfiguration()
{
return Fluently.Configure()
.Database(SetupDatabase)
.Mappings(m => m.AutoMappings.Add(CreateMappingModel()))
.ExposeConfiguration(ConfigurePersistence)
.BuildConfiguration();
}
DotNetOpenAuth Authentication
The project leverages the standard DotNetOpenAuth Authentication that gets added when building a new MVC3 Web Application within Visual Studio but it is tweaked
in that upon successful authentication, the system checks to see if the email is already in the database and if it is, then the user is mapped to a particular player.
If the email doesn't exist, then they are brought to a screen where they can choose which Player they are and then the relationship is created. If this were a production level application,
this would need to get tweaked a little, but for what the application was needed for, it was just enough.
AccountController.cs
[HttpPost]
public ActionResult MapPlayer(FormCollection collection)
{
var user = new User
{
Username = HttpContext.User.Identity.Name,
Email = collection["Email"],
Player = team.GetPlayer(int.Parse(collection["PlayerId"]))
};
users.SaveUser(user);
NHibernateUtil.Initialize(user.Player);
Session["CurrentUser"] = user;
return RedirectToAction("Index", "Team");
}
Team View
Controller
The TeamController
gets an ITeamRepository
injected into it which allows for it to get all the Team Members and the Stats.
public class TeamController : Controller
{
private readonly ITeamRepository team;
public TeamController(ITeamRepository team)
{
this.team = team;
}
public ActionResult Index()
{
ViewBag.Players = team.GetPlayers().OrderBy(p => p.Name).ToList();
ViewBag.Stats = team.GetStats().OrderBy(s => s.Order).ToList();
return View(ViewBag);
}
View
The view builds out a table by using Razor syntax to iterate through the Stats in the ViewBag and then each Player on the team. There is probably some room for performance
optimization or better caching here because of the way XP and Leveling work. The GetLevel
method off of the PlayerStat
has some work in it and this method gets called
for each player for each stat. So on the Team Page, if you have 10 players and 10 stats, the method would get called 100 times.
There may be some presentation guys out there that see those Table
tags and cringe, but I am pretty sure if your content is truly data and belongs in a table, then using
a table instead of div
tags is acceptable.
A jQuery plug-in called TableSorter is leveraged to allow the user to sort the table by a particular stat.
A jQuery plug-in called jQueryTools is used for the tooltips and on the Player Detail page, the animated progress
bar leverages Twits jQuery Progress Bar.
<table id="teamTable" class="tablesorter">
<thead>
<tr>
<th>
Name
</th>
@foreach (var stat in ViewBag.Stats) {
<th>
<span class="abbreviation">@stat.Abreviation</span>
<div class="tooltip">
@stat.Name - @stat.Description
</div>
</th>
}
</tr>
</thead>
<tbody>
@foreach (var player in ViewBag.Players) {
<tr>
<td class="playerName">
@(Html.ActionLink((string)player.Name, "Player",
"Team", new { id = player.Id }, null))
</td>
@foreach (var stat in player.GetCompletePlayerStats(ViewBag.Stats)) {
<td>
@{var level = stat.GetLevel();}
@{var title = string.Format("level {0}: {1}",
level, LaxCraftSession.LevelTitles[level]);}
@{var src = Url.Content("~/content/images/" + level + ".png");}
<img src="@src" alt="@title" title="@title" />
</td>
}
</tr>
}
</tbody>
News Postings
Controller
The Post Controller is fairly straightforward where it will get an IPostRepository
injected into it and it will use that repository to grab all the posts to display.
private readonly IPostRepository blog;
public PostController(IPostRepository blog)
{
this.blog = blog;
}
public ActionResult Index()
{
ViewBag.Posts = blog.GetPosts().OrderByDescending(p => p.CreatedOn);
ViewBag.Admin = LaxCraftSession.CurrentUser.IsAdministrator;
return View(ViewBag);
}
View
The Post View will iterate through each post and display the HTML and the meta data including the Tags and Author as well as show links to Edit, Delete, or Create if the user is an Admin.
@foreach (var post in ViewBag.Posts) {
<div class="postTitle">@post.Title</div>
<div class="postBody">
@post.GetHtml()
</div>
<div class="postFooter">
@string.Format("Posted by {0} on {1:g}", post.CreatedBy, post.CreatedOn);
<br />
@if (!string.IsNullOrEmpty(post.Tags)) {
foreach (var tag in post.Tags.Split(",".ToCharArray())) {
<span class="tag">@tag</span>
}
}
</div>
if (ViewBag.Admin) {
<div class="adminAction">
@Html.ActionLink("Edit", "Edit", new { id = post.Id })
@Html.ActionLink("Delete", "Delete",
new { id = post.Id }, new { rel = "nofollow" })
</div>
}
}
WYSIWYG Editing
The Create and Edit View for a Post uses Telerik's MVC3 Extensions to have a nice Rich Text Editor.
@Html.ValidationSummary(true)
<fieldset>
<div class="editor-label">
@Html.LabelFor(model =>
And here is the method in the Controller that will handle the Form post and save the new post to the database:
[HttpPost]
[ValidateInput(false)]
public ActionResult Create(Post post)
{
if (ModelState.IsValid)
{
var newPost = new Post { Body = post.Body, Tags = post.Tags,
Title = post.Title, CreatedBy = post.CreatedBy,
CreatedOn = DateTime.Now };
blog.Save(newPost);
return RedirectToAction("Index");
}
return View(post);
}
History
- Article submitted: 8/1/2011.