November 2009 - Posts

ASP.NET MVC is a wonderful thing.  One of the many great features is the ability to customize all of the .NET framework's code by swapping out one implementation and using another.  One such instance is creating a custom view engine, which you can do as illustrated in this example: http://www.singingeels.com/Articles/Creating_a_Custom_View_Engine_in_ASPNET_MVC.aspx.  The point of my article is not to illustrate how this can be done, but about how to customize it for your needs.  By default, the web forms view engine looks for views in the folder of the controller or the shared folder.  So if you try to trigger an action method "Index" within the controller of type CustomerController, a partial view (.ascx) or the view (.aspx) is sought for in the ~/Shared folder or ~/Customer folder.

Now, I tend to like to use partial views in order to separate and reuse functionality a lot.  So I tend to have a lot of partial views that tend to get reused across pages and I don't want everything to be in the shared folder (by default, partial views have to be in the shared folder or in the same folder as the controller).  So I added some code to the view engine that allowed me to create subfolders within the shared folder and for the view engine to look for the classes there.  Imagine this folder structure:

Shared
    Customers
    Orders
    Products

So the shared folder breaks up my partial views into the folder above. 

Thinking long-term, rather than hard-coding all these folder references and assigning them to the ViewLocationFormats and PartialViewLocationFormats properties, I wanted something that I wouldn't have to worry about changing later.  So in true ASP.NET MVC framework form, I created some extra code to create the ability to automatically add references to subfolders too.  In order to do this, it's required to use the VirtualPathProvider class to extract the URL, as in the following code:

public MyViewEngine() {
var locations = new List<string>
{
    "~/Views/{1}/{0}.aspx",
    "~/Views/{1}/{0}.ascx",
    "~/Views/Shared/{0}.aspx",
    "~/Views/Shared/{0}.ascx"
};

var dir = this.VirtualPathProvider.GetDirectory("~/Views/Shared");
var subs = dir.Directories.OfType<VirtualDirectory>();

foreach (var sub in subs)
{
    locations.Add("~" + sub.VirtualPath.Substring(sub.VirtualPath.IndexOf("/", 2)) + "{0}.ascx");
}

base.ViewLocationFormats = locations.ToArray();
base.PartialViewLocationFormats = base.ViewLocationFormats;
}

This is the constructor for the custom view engine.  It contains some additional code to use the VirtualPathProvider property (a property of our custom view engine) to extract the subdirectories of the shared folder.  You see the four hard-coded references at the beginning, and so we need to create virtual path strings (which start with "~" and work from the beginning of the virtual directory) to add to the custom list.  When working with folders using VirtualPathProvider, the issue becomes the way paths are referenced.  By default, the path may be:

/MyVirtualFolder/Views/Shared/Customers/

When you need:

~/Views/Shared/Customers/

And so some additional work to format the path is needed (the substring strips off the virtual directory folder.  Now we have a component that will allow the MVC framework to look for partial views in all subdirectories in the shared folder.

Posted by bmains | 1 comment(s)
Filed under: ,

I was going back through an application I am currently working on for a client and trying to come up with a way to bridge a gap between that application and a new web site they want, which has two separate application architectures and ORM solutions.  I realized how some of my errors have come to light, and how the SRP principle could have came in handy.  Let me explain.

When using an ORM solution like LINQ to SQL or ADO.NET Entity Framework, each of these ORM's generates its own class and so you can't simply reuse these objects if each project has it's own class definition generated.  One of my coding blunders has come to light in relation to SRP. Imagine having a plain old CLR object (POCO) that receives data from an ORM generated class as such:

//Class to receive data from a Customer LINQ to SQL class
public class CustomerData
{
   public int Key { get; set; }
   public string Name { get; set; }
   .
   .
}

One of the convenient ways to load this object that I would do would be to add a method that does this, within CustomerData:

public static CustomerData FromCustomer(Customer linqToSqlObj)
{

    return new CustomerData
    {
        Key = linqToSqlObj.Key,
        Name = linqToSqlObj.Name
    };
}

And so this static method helped quickly create an instance.  The problem with this approach is that now I have a class customized for my LINQ to SQL project.  If I want to say use it for a shared project, a project that's shared across multiple projects which may have multiple ORM's, this shared project can't have access to the data access layer (well it shouldn't if it's a shared project) and so the challenge then becomes how can you create this class?

There are a couple of approaches.  One I've used is to use extension methods.  This allows an attachment of a method to do the conversion in a project that can access both objects.  This class could look like:

public static class CustomerDataExtensions
{
    public static CustomerData FromCustomerFromCustomer(this Customer linqToSqlObj)
    {

        return new CustomerData
        {
            Key = linqToSqlObj.Key,
            Name = linqToSqlObj.Name
        };

    }
}

This extension method appends a conversion method onto the LINQ object.  In this way, the responsibilities are separated, even though they may appear not to be.

Posted by bmains | with no comments
Filed under: ,

I got this error when I tried to run this code in my view.

<img src='<%= Url.Content("~/Content/Images/logo.gif") %>' />

At first I didn't realize the underlying issue but then quickly realized that when the MVC framework can't find a resource available on disk, it attempts to serve up the resource using a controller.  My folder structure actually had changed during the project and no the logo resided in Content/Images/Framework, but the URL was using the old directory.

When it can't find the file being accessed, the framework attempts to route the file through the route table established on the global.asax, which is exactly what it was trying to do; it was looking for a controller with the name ContentController, with an Images action method.  And since that can't be found, nothing gets returned.

The RouteTable.Routes property (of RouteCollection type) returns a RouteExistingFiles property.  The default value is false, meaning files on disk do not go through the routing process.  If you set this value to true, existing file routes go through the routing process, which requires a controller.

Posted by bmains | with no comments
Filed under: ,

I've been just getting into MVC lately and run into a weird error, mostly weird because it's mostly due to the error message itself than the actual problem.  The problem is very easy to fix, but first, a little background.  In the various MVC frameworks out there, there are two ways to render controls: using these two syntaxes:

<%= Html.HelperMethod() %>

<% Html.HelperMethod(); %>

Note the syntaxes; the first syntax returns a string.  This string gets rendered directly to the output stream.  The second option uses the current context to render the HTML directly to the browser via the HttpResponse.  It's up to you to determine which of these options the helper method you are currently using, which is helped along using the inline documentation.  But I ran into an interesting situation: if you forget (or don't realize) that the options you are using happen to use the <%= %> syntax and includes a semi-colon, you may get the ) expected error.  Why, I don't know, but that's what I been running into.

Some of the issues may be with the HTML helper themselves; for instance, some of the features you may use may need to use the latter option of rendering rather than the former, which seems to have been what my issue was, but I haven't yet placed my entire finger on it.

Posted by bmains | with no comments
Filed under: ,