Thursday, January 27, 2011

Adding MVC view file extension exclusion to T4MVC

Yesterday I wrote about creating razor view engine code behind files. I've been playing with the T4MVC T4 template for ASP.NET MVC that I learned about by watching this video.

Today I discovered that the view code behind files I created are showing up in the MVC.<controller>.Views list because the files (*.cs) are in the views folders next to the actual views (*.cshtml). Due to the naming convention I used, the *.cs file comes first in the directory listing. While the T4 template code gracefully deals with files with the same name portion of the full filename, the consequence of my names resulted in the generation of the following two properties:
MVC.Shared.Views.PlantListPager
MVC.Shared.Views.PlantListPager_cshtml

The first is the result of the code-behind file (PlantListPager.cs) while the second comes from the actual view (PlantListPager.cshtml). Again, the template gracefully deals with a potential name conflict. However, access to the code-behind path is not needed and the resulting extra property looks messy.

I looked through the T4MVC.settings.t4 file and found the ExcludedStaticFileExtensions array of extensions that excludes the specified file extensions from property generation for the files found in the static folders (as specified by the StaticFilesFolders array just above it in the settings). Unfortunately, I didn't find a similar array specifying exclusions for view folder(s)'s files. Presumably, this is because the view folders would typically not have anything other than actual view files. So I added the following block to the settings:
// View file extensions to exclude from the generated links   
readonly string[] ExcludedViewFileExtensions = new string[] {
".cs"
};

In the T4MVC.tt template file, I found the code that excludes the static files in the "ProcessStaticFilesRecursive" method after which I modeled the following block that I added to the "AddViewsRecursive" method:
if (ExcludedViewFileExtensions.Any(extension =>
item.Name.EndsWith(extension, StringComparison.OrdinalIgnoreCase)))
continue; // ignore defined extensions

This checks for extension exclusions when the list of views to create as properties is first built up.

Now I realize that I should probably just contribute this change back into the MvcContrib project. However, I'm going to cop out for the following reasons:

  • I don't have the infrastructure tools and knowledge required (Mercurial & NUnit)

  • I thought it would be valuable to illustrate how relatively simple it is to make a change to an existing T4 template without totally messing it up.


If there's a T4MVC contributor out there reading this who'd like to include this change, by all means, please do.

Wednesday, January 26, 2011

ASP.NET MVC3 - Creating razor view engine code behind files

While code behind files are generally discouraged for MVC views, there are still times when it's helpful to have strongly typed and pre-compiled code to support a view when you have something more than rudimentary logic involved with view processing.

In order for a single Razor view to have a code-behind file, it must be changed to inherit from another class instead of the default view page class, which is defined in the ~/Views/web.config as
System.Web.Mvc.WebViewPage

The view base class is modified with this addition to the view's markup file:
@inherits <class name>

For example, a code-behind class for ~/Views/Home/Index.cshtml would be
@inherits MySite.Views.Home.Index

If you have a model specified for the view using the @model nugget you must remove it from the view code and apply it to the new base class.

Next, create a new class file for the code-behind class. Following convention, it's probably best to put the file in the same directory as the view file itself and name it similarly. I just grab the view's file name and drop the "html" from the extension. Thus, following our example, we end up with:
~/Views/Home/Index.cs
~/Views/Home/Index.cshtml

The generated class file needs to be modified in these ways:

  • Set the base class to WebViewPage, or WebViewPage<model type> if you had a model type specified in the view code.

  • Add "abstract" to the class definition (a concrete implementation of a WebViewPage derived class must implement the "Execute" method)

Continuing with our example we have a code-behind file for the Home/Index view that looks like this:
namespace MySite.Views.Home
public abstract class Index : WebViewPage
{
// ... my super-duper code goes here
}
}

Now code away! Be sure to expose any methods you want to call from the view code as "protected" or "public" so they are visible to the view's derived class.

Just remember that a view code-behind file is not a substitute for proper separation of concerns. Remember to keep the code you place in a view code-behind class limited to only logic required to make the view do what it needs, no more. Any code that starts to smell of business/application logic should be moved to the appropriate place, either a business library or a controller.

Saturday, January 15, 2011

ASP.NET MVC Razor - AJAX partial page updating using views with default layout

ASP.NET MVC controller action methods that return views don't care whether that view is technically implemented as a full view or a partial view. You can just as easily return a view created as a partial as easily as a standard, full view and the controller cares not. Nor, it seems, does the view engine. It will happily render out the content of the view as it is defined in its markup file. This provides a handy mechanism by which a partial view can be used to perform partial page updates via AJAX. An initial load of a full view can include a call to render a partial view, then a subsequent call from AJAX can explicitly return just the partial view. I've used this technique several times with the original ASPX view engine and not had any issues. However, once I started playing with the Razor view engine, I encountered a strange side effect.

Both the ASPX and Razor view engines can define the master page/layout to use at a global level. For ASPX it's in the web.config:
<pages masterPageFile="~/Views/Shared/Site.Master">
...
</pages>
For Razor it's in the view start file for your chosen language, in my case it's C# (_ViewStart.cshtml):
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}

ASPX view engine partial views (*.ascx) include (apart from the obvious file extension difference) a specific directive instructing the view engine that it is a partial view (@ Control) and not a regular view (@ Page). This causes the view engine to knowingly treat it as a partial view, rendering only its contents. However, since Razor views are all the same there is no declaration to indicate that the view code in one .cshtml file is for a partial view versus a full view. Any Razor view will have the same startup rules applied.

When a Razor view is rendered as a partial from another view, such as:
@Html.Partial("mypartialview")
or
<% Html.RenderPartial("mypartialview"); %>

the view engine seems to recognize that the view is being loaded as a partial and (presumably) changes the inner view's Layout property to null and things work as desired.

However, when you make an MVC action call that specifically loads JUST a partial view as the ViewResult with the intent of doing a partial page refresh, the results will differ based on the the view engine. For an ASPX engine partial view (i.e. *.ascx) the view will load and render as is. It contains no references to a master page and the view engine knows explicitly that it is a "@ Control" and thus does not wrap it in any default master page, if one is defined. On the other hand, a Razor view is loaded as any other view and will include the master layout set by default. What is intended as a partial page update will actual return with all of the layout pages HTML around it. In order to keep a Razor view as a partial view, any existing value for the view's Layout property must be negated. This code can be placed in a code block of any view that is intended to be a partial view:
@{
this.Layout = null;
}

This will cancel out any default master layout setting and render the view with only that view's contents. When the same view is included in another view as a partial, the above code will have not effect since the calling view has already nulled out the layout file anyway.

If you wanted to, you could use a view for either a full view (with the default layout's content) or a partial view by conditionally nulling out the Layout property based on some view data set by an appropriate controller action. Combined with a parameter passed in by the AJAX caller, this could provide a useful mechanism for reusing view elements.

Friday, January 14, 2011

Doing 2+ aggregations in LINQ to SQL

I was recently trying to do a simple double aggregation using LINQ to SQL.

Here's the setup:

One table "Plant" containing these two fields of concern:
  ZoneMin tinyint
ZoneMax tinyint

There's a search view on which I want to include the selection of a range for each of those fields. Therefore, I want to select the minimum and maximum of each field. In T-SQL this would be trivial:
SELECT
MIN(ZoneMin) MinMin, MAX(ZoneMin) MinMax,
MIN(ZoneMax) MaxMin, MAX(ZoneMax) MaxMax
FROM Plant

I tried numerous approaches with the .Aggregate() method, grouping and everything else I could think of. No luck. I finally broke down and posted to StackOverflow and got a simple answer. The end result is this LINQ query:
Plants.GroupBy (p => 0).Select (p => new { 
MinMin = p.Min (x => x.ZoneMin),
MinMax = p.Max (x => x.ZoneMin),
MaxMin = p.Min (x => x.ZoneMax),
MaxMax = p.Max (x => x.ZoneMax)
})

This yields the following T-SQL (courtesy of LINQPad):
-- Region Parameters
DECLARE @p0 Int = 0
-- EndRegion
SELECT
MIN([t1].[ZoneMin]) AS [MinMin], MAX([t1].[ZoneMin]) AS [MinMax],
MIN([t1].[ZoneMax]) AS [MaxMin], MAX([t1].[ZoneMax]) AS [MaxMax]
FROM (
SELECT @p0 AS [value], [t0].[ZoneMin], [t0].[ZoneMax]
FROM [Plant] AS [t0]
) AS [t1]
GROUP BY [t1].[value]

Comparing the two execution plans (Query 2 is LINQ and Query 3 is T-SQL) shows that they are the same:



So the lesson here is: remember to try constant value grouping.