Displaying Custom ASP.NET MVC Views per Deployment

Have you ever been asked to deploy a second (or third or fourth) instance of an ASP.NET MVC app but with minor customizations to the views? Maybe it’s something as simple as a logo or color change, but more likely it is much more elaborate customizations that require completely different layouts and view structure.

One option would be to add application settings for each individual view variation then add conditional logic to your razor views so they can render differently based on those settings. This might work for very simple cases but it will quickly becomes unwieldy.

I have found an option using a custom view engine that is surprisingly simple. Here’s how it works.

View Folder Structure

We will implement a custom view engine that will extend the existing View folder layout to support custom views per deployment. By default, everything works just like the base MVC rendering engine. If you need to customize a view for a particular deployment, just add views in a sub-folder with a name that is unique to that deployment.

In the example below, there is a custom Contact.cshtml view for deployments using Client1 and _Client2. _There is also a default Contact.cshtml that is used for any other deployments. The custom views are optional. The flexibility here I great because it allows us to put anything we want in the custom Client1 and Client2 views.

Note that the custom views are completely optional. In the example above, there are no customizations for the About.cshtml and Index.cshtml pages. All deployments will use the default views.

Custom View Engine

To make this all work, we need to implement a custom view engine that knows to look for custom views before falling back to the default views.

This is actually much easier than I had anticipated. The constructor for our custom view engine takes in a string indicating the name of the folders that would contain the custom views. Using that folder name, all it needs to do is specify new view location formats for the RazorViewEngine base class.

public class CustomViewEngine : RazorViewEngine
{
    public CustomViewEngine(string customViewName)
    {
        base.AreaViewLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"

        };

        base.AreaMasterLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"

        };

        base.AreaPartialViewLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"
        };

        base.ViewLocationFormats = new string[] {
            "~/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml",               
        };

        base.PartialViewLocationFormats = new string[] {
            "~/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };

        base.MasterLocationFormats = new string[] {
            "~/Views/{1}/" + customViews + "/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/{1}/" + customViewName + "/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };
    }
} 

Note that I did not include *.vbtml because my app only uses cshtml files.

In web.config, add an app setting to specify which custom view folder you want to use:

  <appSettings>
    <add key="CustomViewName" value="Client1"/>
  </appSettings>

 

And finally, set the view engine in the Global.asax application start method

ViewEngines.Engines.Clear();
string customViewName = ConfigurationManager.AppSettings["CustomViewName"];
ViewEngines.Engines.Add(new CustomViewEngine(customViewName));

 

Now, by changing a setting in Web.config, we can show custom views in different deployments of the application.

Keeping it manageable

This can very quickly become difficult to manage. One trick is to use partial views for the portion of a page that might be different between deployments. For example, if the top navigation header is different per deployment, then I would extract the navigation header to a partial view and create custom version of that partial.

This way, my base _Layout.cshtml stays simple and is the same for all deployments.

<body>
    @Html.Partial("_NavHeader")
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>

…and I am able to override the navigation header as needed.

Conclusion

Now we have a simple approach that allows us to have customized views for individual deployments of our ASP.NET MVC application. Keep in mind that this approach only addresses the problem of customizing views between deployments. It does not in any way address the problem of having custom application logic in your controllers or services.

2015 MVP Virtual Conference–May 14-15

I wanted to let you know about a great free event that Microsoft and the MVPs are putting on, May 14th & 15th. A group of great Microsoft MVPs will be sharing knowledge and real-world expertise during a free event, the MVP Virtual Conference.

MVP15_MicrosoftMVP_VC_WebBanner_920x400px

The MVP Virtual Conference will showcase 95 sessions of content for IT Pros, Developers and Consumer experts designed to help you navigate life in a mobile-first, cloud-first world. Microsoft’s Corporate Vice President of Developer Platform, Steve Guggenheimer, will be on hand to deliver the opening Key Note Address.

Why should you attend?

Because the content will be awesome! Go ahead and review the agenda. The conference will have 5 tracks, IT Pro English, Dev English, Consumer English, Portuguese mixed sessions & Spanish mixed sessions, there is something for everyone! Learn from the best and brightest MVPs in the tech world today and develop some great skills!

Be sure to register quickly to hold your spot and tell your friends & colleagues.

Stop writing vendor prefixes

I hate writing vendor prefixes in my CSS. I am constantly having to look up what vendor prefixes are needed for different CSS properties. The list is constantly changing as browsers settle on official standards. A great example is border-radius. In the past, you needed to use the following vendor prefixes:

-moz-border-radius: 2px 2px 2px 2px;
-webkit-border-radius: 2px 2px 2px 2px;
border-radius: 2px 2px 2px 2px;

 

But a while ago, browser vendors settled on a standard for border-radius and now the vendor prefixes are no longer needed.

border-radius: 2px 2px 2px 2px;

 

Keeping up with the changes involves either following the W3C CSS specs very closely and/or constantly checking http://caniuse.com/.

Autoprefixer

It turns out that there is a great tool called Autoprefixer that can add vendor prefixes for you automatically. Autoprefixer can parse your CSS and automatically add vendor prefixes based on information from caniuse. You can even specify exactly what browsers you would like to target so autoprefixer doesn’t need to add vendor prefixes that your application does not need.

Not only does autoprefixer add missing vendor prefixes, it is also smart enough to remove vendor prefixes that are no longer needed. For example, –moz-border-radius and _–webkit-border-radius _would be removed for you.

Gulp Autoprefixer

The easiest way to use autoprefixer is by using a task runner like Gulp or Grunt. Building on my previous examples of using Gulp and Bower with Visual Studio, we can simply install the gulp-autoprefixer npm package:

npm install gulp-autoprefixer –save-dev

Here is a very simple gulp task that will process a CSS file and output a minified and non-minified version of that CSS file with the vendor prefixes added.

 

var gulp = require('gulp');
var concat = require('gulp-concat');
var minifyCSS = require('gulp-minify-css');

var config = {
    appcss: 'Content/Site.css',
    cssout: 'Content/dist/css'
}

gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(autoprefixer())
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});

When I run this task, the CSS file _Content/Site.css _is processed by autoprefixer and then output to Content/dist/css/app.css. A minified version is also output to the same folder.

In my _Layouts.cshtml, I link to the generated Content/dist/css/app.min.css file.

<link href="~/Content/dist/css/app.min.css" rel="stylesheet" />

 

Whenever the CSS gulp task runs, the output files will be regenerated. For example, the following CSS from Site.css:

.fancy-flex-section {
    display: flex;
}

.fancy-rounded {
    -moz-border-radius: 2px 2px 2px 2px;
    -webkit-border-radius: 2px 2px 2px 2px;
    border-radius: 2px 2px 2px 2px;
}

 

will be processed by autoprefixer and output as:

.fancy-flex-section {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}

.fancy-rounded {
    border-radius: 2px 2px 2px 2px;
}

 

Specify Target Browsers

Specifying the browsers you want to target is simply a matter of passing a parameter to autoprefixer. Under the covers, autoprefixer uses Browserlist, which gets browser usage data from caniuse. A common approach is to support the last 2 versions of all major browsers.

gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(concat('app.css'))
     .pipe(autoprefixer({ browsers: ['last 2 version'] }))
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});

Another approach is to support any browser that has more than x% market share:

gulp.task('css', function () {
    return gulp.src([config.appcss])
     .pipe(concat('app.css'))
     .pipe(autoprefixer({ browsers: ['> 5%'] }))
     .pipe(gulp.dest(config.cssout))
     .pipe(minifyCSS())
     .pipe(concat('app.min.css'))
     .pipe(gulp.dest(config.cssout));
});

The list of options for targeting browsers is impressive. You can even target browsers based on usage in a specific country. For a complete list, go check out Browserlist.

Conclusion

With autoprefixer, you no longer need to worry about writing vendor prefixes in your CSS. Simply write your CSS in un-prefixed format and let autoprefixer do the rest for you.

Integrating Gulp and Bower with Visual Studio Online Hosted Builds

In previous posts, I talked about using Gulp to manage client side builds and using Bower as a package manager for client side packages.

Shane Courtrille asked me if I could do a follow up post showing how to integrate these concepts with a build hosted on Visual Studio Online. It took me some time and a long list of failures to get this working, but here it is.

Build Server Requirements

In order to successfully use Gulp and Bower on your build server, you will need both Node and Git installed. Luckily, the hosted Visual Studio Online build controllers already have these tools installed. If you are hosting your own build server, you will need to install node.js and Git on the server. When installing these tools, make sure you login to the server as the same user that executes the build process.

Adding a PreBuild Script to Visual Studio Build

The next step is to ensure that we call the default gulp task before attempting to compile our application with msbuild. Using the gulp file from the blog post on using Bower, calling the default gulp task will ensure that all bower packages are installed and all our JavaScript and CSS bundles are created and copied to the expected locations.

Gulp requires a number of node packages to be installed. Before we can call gulp, we need to call npm install to ensure that those packages have been downloaded locally on the build controller.

The easiest way to do this is to create a simple PowerShell script that is executed as a Pre Build step for our Visual Studio build. Pre Build Steps are only available if GitTemplate.12.xaml as your build process template.

I added a PreBuild.ps1 script to the root folder for my solution and added that file as my Pre-build script path.

Push-Location "BowerInVS2013"
& npm install
& gulp default
Pop-Location

In theory, that’s all we needed to do. Unfortunately, theory and practice are rarely aligned…especially when working with Visual Studio builds.

When I tried to run this build in Visual Studio Online, I got the following error:

Error: ENOENT, stat 'C:\Users\buildguest\AppData\Roaming\npm'

For some reason, the buildguests user that runs the build on Visual Studio online has trouble with the call to npm install. A quick workaround for this is to create the missing npm folder if it does not exist.

$appdatafolder = [Environment]::GetFolderPath('ApplicationData')
$npmfolder = $appdatafolder + "\npm"
if(!(Test-Path -Path $npmfolder)){
New-Item -Path $npmfolder -ItemType Directory
}
Push-Location "BowerInVS2013"
& npm install
& gulp default

Pop-Location

This brings us to my next failure: Gulp is not installed globally on the hosted build controller so the call to gulp default failed.

To solve this I added added a gulp script to my npm package file (package.json). This gives me a convenient way to call gulp via npm.

{
"name": "BowerInVS2013",
"version": "1.0.0",
"private": true,
"devDependencies": {
"del": "^1.1.1",
"gulp": "^3.8.10",
"gulp-bower": "0.0.7",
"gulp-concat": "^2.4.2",
"gulp-copy": "0.0.2",
"gulp-minify-css": "^0.3.11",
"gulp-sourcemaps": "^1.3.0",
"gulp-uglify": "^1.0.2"
},
"scripts": {
"gulp": "./node_modules/.bin/gulp"
}
}

The updated PreBuild.ps1 script looks like this:

$appdatafolder = [Environment]::GetFolderPath('ApplicationData')
$npmfolder = $appdatafolder + "\npm"
if(!(Test-Path -Path $npmfolder)){
New-Item -Path $npmfolder -ItemType Directory
}
Push-Location "BowerInVS2013"
& npm install
& npm gulp
Pop-Location

We still get one warning from npm and I’m not sure how to fix it.

npm WARN deprecated deflate-crc32-stream@0.1.2: module has been merged into crc32-stream

This warning seems to be a known issue with node on Windows and in this case it seems to be safe to ignore it. The compiled output from the build contains everything that we expect.

Conclusion

With a few minor workarounds, we are able to get Bower and Gulp working with the Visual Studio Online hosted build controllers.

Prairie Dev Con Wrap Up

I just returned from Prairie Dev Con (PrDC) in Winnipeg. As with every previous Prairie Dev Con, I was not disappointed. Every PrDC I have attended has been fantastic and yet each year it continues to get better.

My Favorite Session

I attended a number of sessions related to improving the performance of web applications. I learned something in every session, but Nik Molnar’s session on Full Stack Web Performance particularly stood out for me. I picked up countless web performance tips that I was able to apply to my current project as soon as I got back to work. Some of the tips he covered can be found at http://jankfree.org/.

My Sessions

I was fortunate enough to present two sessions. I have posted the slides on SlideShare for anyone who is interested.

The State of Entity Framework

Visual Studio 2013 with Gulp and Bower

Sessions I did not get to attend

There are two sessions that everyone was talking about and I was unable to attend because they were at the same time as my sessions. Luckily for me (and you), these sessions were recorded and are available on YouTube:

The Empathy Equation – Dave Mosher

The 5 Stages of Entrepreneurial Grief – Derick Bailey

Waiting for gulp tasks to finish in Visual Studio

In a previous post, I outlined how to use Bower in Visual Studio 2013. While this code works for the typical developer workflow, a reader pointed out that errors were observed when attempting to publish to Azure using the Visual Studio publish dialog.

After further investigation, I was able to determine the reason for this error is due to the way Task Runner Explorer executes tasks. While we setup the binding so that our default Gulp tasks runs Before Build, this does not fully integrate into the Visual Studio build pipeline. The Gulp task is indeed triggered before Visual Studio starts to build the project, but Visual Studio does not wait for the task to complete before building the project. As a result, if your VS project is small enough, Visual Studio might think the build is completed before our Gulp task is completed. This is the reason for the Publish failing: Visual Studio was proceeding with publishing to Azure before all the required files were generated.

A Potential Workaround

Eventually, there will be better support for this in future versions of Task Runner Explorer. In the meantime, we need a simple workaround. One option is to run a script as a Post Build Event for my project to wait for all the generated files to exist. I did this as a simple PowerShell script file named WaitForFiles.ps1:

$start = $(Get-Date)
$rootPath = $args[0]

#Files to wait for
$bootstrapBundle = $rootPath + "Scripts\bootstrap-bundle.min.js"
$jqueryBundle = $rootPath + "Scripts\jquery-bundle.min.js"
$modernizer = $rootPath + "Scripts\modernizer-min.js"
$css = $rootPath + "Content\dist\css\app.min.css"
$glyphicons = $rootPath + "Content\dist\fonts\glyphicons-halflings-regular.*"

#Delay 2 seconds to ensure the gulp tasks were started (ie. Make sure we aren't testing for files before they were deleted and regenerated)
Start-Sleep -Seconds 2
while(!((Test-Path $bootstrapBundle) -and (Test-Path $jqueryBundle) -and (Test-Path $modernizer) -and (Test-Path $css) -and (Test-Path $glyphicons))) {
    Write-Output "Waiting for generated files"
    Start-Sleep -Seconds 2
    $elapsedTime = $(Get-Date) - $start
   #Wait a maximum of 60 seconds for the files to appear
    if ($elapsedTime.TotalSeconds -gt 60){
        Write-Error "Timed out while waiting for generated files"
        exit -2
    }
}
exit 0

Now, execute this script in the Post Build event of the web project. You can set the post build event from the Build Events tab of the Project Properties page.

 

Powershell -ExecutionPolicy bypass -File "$(ProjectDir)WaitForFiles.ps1" "$(ProjectDir)\"

While this is not an ideal solution, it should fix the problem of Visual Studio attempting to publish your application before the required files have been generated by your Gulp tasks. In future versions of Visual Studio I would expect this integration to be a little cleaner.