Have you ever needed to detect what part of the application is currently being viewed? This might be a bigger issue if you write a lot of shared/partial views or custom display or editor templates. Another scenario, which is the one I encountered when I first started down this path, is when you have some type of menu and you’d like to be able to determine which item represents the current page so you can highlight it in some way. A simple example is the menu that is created as part of the default ASP.NET MVC 2 Application template.
<div id="menucontainer">
<ul id="menu">
<li><%= Html.ActionLink("Home", "Index", "Home") %></li>
<li><%= Html.ActionLink("About", "About", "Home") %></li>
</ul>
</div>
The part that got me at first, however, was the following entry in the default style sheet (Site.css):
ul#menu li.selected a
{
background-color: #fff;
color: #000;
}
I assumed that the .selected class would automatically get applied to the active menu item. After trying a few different things, including the MvcContrib MenuBuilder, I decided to write my own extension methods so I would have more control over the output. First, I needed a way to determine what view the user has navigated to based on the requested URL and route configuration. Now, I am sure there are many ways to do this, but this is what I came up with:
public static class RequestExtensions
{
public static bool IsCurrentRoute(this RequestContext context, String areaName,
String controllerName, params String[] actionNames)
{
var routeData = context.RouteData;
var routeArea = routeData.DataTokens["area"] as String;
var current = false;
if ( ((String.IsNullOrEmpty(routeArea) && String.IsNullOrEmpty(areaName)) ||
(routeArea == areaName)) &&
((String.IsNullOrEmpty(controllerName)) ||
(routeData.GetRequiredString("controller") == controllerName)) &&
((actionNames == null) ||
actionNames.Contains(routeData.GetRequiredString("action"))) )
{
current = true;
}
return current;
}
// additional overloads omitted...
}
With that in place, I was able to write several UrlHelper methods that check if the supplied values map to the current view.
public static class UrlExtensions
{
public static bool IsCurrent(this UrlHelper urlHelper, String areaName,
String controllerName, params String[] actionNames)
{
return urlHelper.RequestContext.IsCurrentRoute(areaName, controllerName, actionNames);
}
public static string Selected(this UrlHelper urlHelper, String areaName,
String controllerName, params String[] actionNames)
{
return urlHelper.IsCurrent(areaName, controllerName, actionNames)
? "selected" : String.Empty;
}
// additional overloads omitted...
}
Now I can re-work the original menu to utilize these new methods. Note: be sure to import the proper namespace so the extension methods become available inside your views!
<div id="menucontainer">
<ul id="menu">
<li class="<%= Url.Selected(null, "Home", "Index") %>">
<%= Html.ActionLink("Home", "Index", "Home")%></li>
<li class="<%= Url.Selected(null, "Home", "About") %>">
<%= Html.ActionLink("About", "About", "Home")%></li>
</ul>
</div>
If we take it one step further, we can clean up the markup even more. Check out the Html.ActionMenuItem() extension method and the refined menu:
public static class HtmlExtensions
{
public static MvcHtmlString ActionMenuItem(this HtmlHelper htmlHelper, String linkText,
String actionName, String controllerName)
{
var html = new StringBuilder("<li");
if ( htmlHelper.ViewContext.RequestContext
.IsCurrentRoute(null, controllerName, actionName) )
{
html.Append(" class=\"selected\"");
}
html.Append(">")
.Append(htmlHelper.ActionLink(linkText, actionName, controllerName))
.Append("</li>");
return MvcHtmlString.Create(html.ToString());
}
// additional overloads omitted...
}
UPDATE: Thanks to Ryan for reminding me to use the TagBuilder class instead of the StringBuilder for generating the HTML for the menu item! Here is the refactored method:
public static class HtmlExtensions
{
public static MvcHtmlString ActionMenuItem(this HtmlHelper htmlHelper, String linkText,
String actionName, String controllerName)
{
var tag = new TagBuilder("li");
if ( htmlHelper.ViewContext.RequestContext
.IsCurrentRoute(null, controllerName, actionName) )
{
tag.AddCssClass("selected");
}
tag.InnerHtml = htmlHelper.ActionLink(linkText, actionName, controllerName).ToString();
return MvcHtmlString.Create(tag.ToString());
}
}
<div id="menucontainer">
<ul id="menu">
<%= Html.ActionMenuItem("Home", "Index", "Home") %>
<%= Html.ActionMenuItem("About", "About", "Home") %>
</ul>
</div>
Which generates the following HTML:
<div id="menucontainer">
<ul id="menu">
<li class="selected"><a href="/">Home</a></li>
<li><a href="/Home/About">About</a></li>
</ul>
</div>
I have created a codepaste of these extension methods if you are interested in using them in your own projects. Enjoy!
Recently, I started working on a new ASP.NET MVC 2 project and I wanted to reuse the data access (LINQ to SQL) and business logic methods (WCF RIA Services) that had been developed for a previous project that used Silverlight for the front-end. I figured that I would be able to instantiate the various DomainService classes from within my controller’s action methods, because after all, the code for those services didn’t look very complicated. WRONG! I didn’t realize at first that some of the functionality is handled automatically by the framework when the domain services are hosted as WCF services. After some initial searching, I came across an invaluable post by Joe McBride, which described how to get RIA Service .svc files to work in an MVC 2 Web Application, and another by Brad Abrams. Unfortunately, Brad’s solution was for an earlier preview release of RIA Services and no longer works with the version that I am running (PDC Preview).
I have not tried the RC version of WCF RIA Services, so I am not sure if any of the issues I am having have been resolved, but I wanted to come up with a way to reuse the shared libraries so I wouldn’t have to write a non-RIA version that basically did the same thing. The classes I came up with work with the scenarios I have encountered so far, but I wanted to go ahead and post the code in case someone else is having the same trouble I had. Hopefully this will save you a few headaches!
1. Querying
When I first tried to use a DomainService class to perform a query inside one of my controller’s action methods, I got an error stating that “This DomainService has not been initialized.” To solve this issue, I created an extension method for all DomainServices that creates the required DomainServiceContext and passes it to the service’s Initialize() method. Here is the code for the extension method; notice that I am creating a sort of mock HttpContext for those cases when the service is running outside of IIS, such as during unit testing!
public static class ServiceExtensions
{
/// <summary>
/// Initializes the domain service by creating a new <see cref="DomainServiceContext"/>
/// and calling the base DomainService.Initialize(DomainServiceContext) method.
/// </summary>
/// <typeparam name="TService">The type of the service.</typeparam>
/// <param name="service">The service.</param>
/// <returns></returns>
public static TService Initialize<TService>(this TService service)
where TService : DomainService
{
var context = CreateDomainServiceContext();
service.Initialize(context);
return service;
}
private static DomainServiceContext CreateDomainServiceContext()
{
var provider = new ServiceProvider(new HttpContextWrapper(GetHttpContext()));
return new DomainServiceContext(provider, DomainOperationType.Query);
}
private static HttpContext GetHttpContext()
{
var context = HttpContext.Current;
#if DEBUG
// create a mock HttpContext to use during unit testing...
if ( context == null )
{
var writer = new StringWriter();
var request = new SimpleWorkerRequest("/", "/",
String.Empty, String.Empty, writer);
context = new HttpContext(request)
{
User = new GenericPrincipal(new GenericIdentity("debug"), null)
};
}
#endif
return context;
}
}
With that in place, I can use it almost as normally as my first attempt, except with a call to Initialize():
public ActionResult Index()
{
var service = new NorthwindService().Initialize();
var customers = service.GetCustomers();
return View(customers);
}
2. Insert / Update / Delete
Once I got the records showing up, I was trying to insert new records or update existing data when I ran into the next issue. I say issue because I wasn’t getting any kind of error, which made it a little difficult to track down. But once I realized that that the DataContext.SubmitChanges() method gets called automatically at the end of each domain service submit operation, I could start working on a way to mimic the behavior of a hosted domain service. What I came up with, was a base class called LinqToSqlRepository<T> that basically sits between your implementation and the default LinqToSqlDomainService<T> class.
[EnableClientAccess()]
public class NorthwindService : LinqToSqlRepository<NorthwindDataContext>
{
public IQueryable<Customer> GetCustomers()
{
return this.DataContext.Customers;
}
public void InsertCustomer(Customer customer)
{
this.DataContext.Customers.InsertOnSubmit(customer);
}
public void UpdateCustomer(Customer currentCustomer)
{
this.DataContext.Customers.TryAttach(currentCustomer,
this.ChangeSet.GetOriginal(currentCustomer));
}
public void DeleteCustomer(Customer customer)
{
this.DataContext.Customers.TryAttach(customer);
this.DataContext.Customers.DeleteOnSubmit(customer);
}
}
Notice the new base class name (just change LinqToSqlDomainService to LinqToSqlRepository). I also added a couple of DataContext (for Table<T>) extension methods called TryAttach that will check to see if the supplied entity is already attached before attempting to attach it, which would cause an error!
3. LinqToSqlRepository<T>
Below is the code for the LinqToSqlRepository class. The comments are pretty self explanatory, but be aware of the [IgnoreOperation] attributes on the generic repository methods, which ensures that they will be ignored by the code generator and not available in the Silverlight client application.
/// <summary>
/// Provides generic repository methods on top of the standard
/// <see cref="LinqToSqlDomainService<TContext>"/> functionality.
/// </summary>
/// <typeparam name="TContext">The type of the context.</typeparam>
public abstract class LinqToSqlRepository<TContext> : LinqToSqlDomainService<TContext>
where TContext : System.Data.Linq.DataContext, new()
{
/// <summary>
/// Retrieves an instance of an entity using it's unique identifier.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="keyValues">The key values.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual TEntity GetById<TEntity>(params object[] keyValues) where TEntity : class
{
var table = this.DataContext.GetTable<TEntity>();
var mapping = this.DataContext.Mapping.GetTable(typeof(TEntity));
var keys = mapping.RowType.IdentityMembers
.Select((m, i) => m.Name + " = @" + i)
.ToArray();
return table.Where(String.Join(" && ", keys), keyValues).FirstOrDefault();
}
/// <summary>
/// Creates a new query that can be executed to retrieve a collection
/// of entities from the <see cref="DataContext"/>.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <returns></returns>
[IgnoreOperation]
public virtual IQueryable<TEntity> GetEntityQuery<TEntity>() where TEntity : class
{
return this.DataContext.GetTable<TEntity>();
}
/// <summary>
/// Inserts the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="entity">The entity.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual bool Insert<TEntity>(TEntity entity) where TEntity : class
{
//var table = this.DataContext.GetTable<TEntity>();
//table.InsertOnSubmit(entity);
return this.Submit(entity, null, DomainOperation.Insert);
}
/// <summary>
/// Updates the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="entity">The entity.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual bool Update<TEntity>(TEntity entity) where TEntity : class
{
return this.Update(entity, null);
}
/// <summary>
/// Updates the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="entity">The entity.</param>
/// <param name="original">The original.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual bool Update<TEntity>(TEntity entity, TEntity original)
where TEntity : class
{
if ( original == null )
{
original = GetOriginal(entity);
}
var table = this.DataContext.GetTable<TEntity>();
table.TryAttach(entity, original);
return this.Submit(entity, original, DomainOperation.Update);
}
/// <summary>
/// Deletes the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="entity">The entity.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual bool Delete<TEntity>(TEntity entity) where TEntity : class
{
//var table = this.DataContext.GetTable<TEntity>();
//table.TryAttach(entity);
//table.DeleteOnSubmit(entity);
return this.Submit(entity, null, DomainOperation.Delete);
}
protected virtual bool Submit(Object entity, Object original, DomainOperation operation)
{
var entry = new ChangeSetEntry(0, entity, original, operation);
var changes = new ChangeSet(new ChangeSetEntry[] { entry });
return base.Submit(changes);
}
private TEntity GetOriginal<TEntity>(TEntity entity) where TEntity : class
{
var context = CreateDataContext();
var table = context.GetTable<TEntity>();
return table.FirstOrDefault(e => e == entity);
}
}
4. Conclusion
So there you have it, a fully functional Repository implementation for your RIA Domain Services that can be consumed by your ASP.NET and MVC applications. I have uploaded the source code along with unit tests and a sample web application that queries the Customers table from inside a Controller, as well as a Silverlight usage example.
As always, I welcome any comments or suggestions on the approach I have taken. If there is enough interest, I plan on contacting Colin Blair or maybe even the man himself, Brad Abrams, to see if this is something worthy of inclusion in the WCF RIA Services Contrib project. What do you think?
Enjoy!