There are plenty of tutorials out there on how to get date-pickers to work in MVC, but most of them that I have seen fall short for some reason or another.  Here are my requirements for a “proper” date-picker in MVC:

  1. It must be automatic, when using the EditorFor family of methods.
  2. It must degrade gracefully. That is, it should fall back to a regular text box for users without JavaScript.
  3. It must always work for any complex model.
  4. It must round-trip the data entered, even if the data is an invalid date.
  5. It must be self-contained.  That is, it should not require a script at the top of the page to work.

Some demos fail to work for complex models, and most require a surrogate script at the top of the page.  We’ll see how to avoid those problems in the coming sections.  I will be assuming that your project already has jQuery and jQuery UI.

Automatic Editor

The ability for MVC to automatically pick an editor based on the data-type has been covered at length before, just search Google for “EditorTemplates” if you are looking for examples.

Quickly, then, here is how to add an automatic template for any type:

  1. Navigate to the ~/Views/Shared folder of your web project.
  2. Create a folder named “EditorTemplates”, if it does not already exist.
  3. Inside of this folder, create a partial view with the name of your type. (DateTime, in our case).

We will also choose to have a strongly-typed view, using DateTime? as our type.  This will allow it to work for both nullable and non-nullable dates

Here is how the folder structure should be laid out:

Date Picker - 1 - Layout

Now, MVC will automatically select our editor whenever it has a DateTime or a Nullable<DateTime> to render.

Round-trippin’ With My Two Favorite Allies

We need to use a regular HTML text box for our date picker for two reasons:

  1. It will allow for date-input, even if the user has disabled JavaScript.
  2. MVC will automatically round-trip the data and perform model binding, if it is rendered in a certain way.

We chose to use a dynamic partial view, so that we could round-trip textual data, above, so the simplest way to get the editor working is this, placed in the DateTime.cshtml file:

@model DateTime?
@Html.TextBox("", Model)

Interestingly, we are passing an empty string in for the “name” parameter for the text box.  This is the secret sauce for getting MVC to render the proper name to support complex models and for it to automatically round-trip the model for.  Second, we are simply passing the model as the value of the text box.  MVC will take this and call ToString() on the value, which will give us the default formatting for dates.

We may, however, want dates to be shown without their time component. To do that, we need to override the default formatting to our liking:

@model DateTime?
@Html.TextBox("", Model.HasValue ? Model.Value.ToString("d MMM yyyy") : "")
This will not prevent the users’ input from round-tripping, but will instead provide a default formatting for views where there is no posted data to reference, e.g. loading the edit view for the first time.

Self-contained Script

Some people prefer to have a single script for wiring-up their date-pickers at the top of the page.  I find this to be less than ideal, because I usually don’t date pickers on a page, so it doesn’t belong in the master layout.  I also don’t want to duplicate the code on every page, because it means that a simple change to the date picker would have to be performed in many places, and it could easily be forgotten on a page, preventing date pickers from working there.

With that in mind, my preference is to have a self-contained script for creating the date-picker immediately following the text box.  The challenge that we face when we want to do this is coming up with a way to refer to the specific text-box in a jQuery selector.  The easiest way, from a jQuery standpoint, would be to use the ID of the text box.

Luckily, MVC allows us access to the field name using the Html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName method.  This is, in fact, the way that MVC assigns the name and ID to the text box.  However, there is no built-in way to turn that name into an ID.  A quick perusal of the MVC source code shows that the ID is a simple text replacement.  We can precisely emulate their text replacement with a regular expression replacement.  Here is the completed control:

@model DateTime?
@{
    var name = Html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty);
    var id = System.Text.RegularExpressions.Regex.Replace(name, @"[^-\w\d:_]", HtmlHelper.IdAttributeDotReplacement);
}
@Html.TextBox("", Model.HasValue ? Model.Value.ToString("d MMM yyyy") : "")<script type="text/javascript">$(function () { $("#@id").datepicker({ dateFormat: 'd M yy' }); });</script>

Now, we have a feature-complete, self-contained date picker, that is automatically applied for all DateTime fields!


 
Categories:

A while ago, I posted about Getting IntelliSense working in ASP.NET MVC 3.  Since the release MVC 3, the primary view engine has changed to Razor, and the previous technique isn’t as elegant.

With that in mind, I searched for a slightly prettier way to get IntelliSense working in Razor views.  I was, unfortunately, unable to get the scripts to be referenced in the views automatically, but I think that this way is clean enough.

I used a @section directive to scoop out a part of the page during rendering, in order to prevent the “vsdoc” scripts files from being rendered to the final output. Here is how it looks in action:

Razor Intellisense - 1 - Working

But now we have a couple of challenges to deal with. If we just ignore the section, we get this:

The following sections have been defined but have not been rendered for the layout page "~/Views/Shared/_Layout.cshtml": "intellisense".

And we know that we don’t want to render it to the final output, so we have to come up with a way to trick the Razor engine into thinking that we did, in fact, render the output.

My first attempt at solving this was to put this into the _Layout.cshtml page:

@{RenderSection("intellisense", required: false);}

But that was not enough to trick the view engine. Apparently, you have to actually render the section somewhere in order for it to be considered “rendered”. Luckily, we can just render it to a throw-away StringWriter, like so:

@{WriteTo(new StringWriter(), RenderSection("intellisense", required: false));}

That is just enough to get it to work properly and display the IntelliSense as desired.

I am still looking for a way to get this to happen automatically, but for now, placing the @section intellisense region at the top of each view will have to do.


 
Categories:

Slick Analytics

What do you do when you need to add analytics to your server?  Well, Google Analytics is a good option, for sure, but it doesn’t capture important, juicy details like bandwidth usage, time to complete requests, cache-hits and so on.  If you need data like this, the best source is going to be the web access logs from your HTTP server.

I’ve taken a look at a few 3rd party tools for creating analytics from these log files, and there are downsides to every one of them.  Cacti is a pretty good option, however it requires a few things that may not be installed on a Windows server (i.e. PHP, MySQL, RRDTool, and net-snmp).  So, based on the fact that most existing solutions aren’t designed to fit seamlessly into a windows environment, I decided to come up with my own solution.

My solution is based on a couple of off-the-shelf Microsoft products that, if you are running on the Microsoft stack, should already be licensed to you.  Here’s the tool chain:

  • IIS
  • Log Parser 2.2 (For turning W3C logs into an easy-to-import format.  It may also support Apache W3C logs, but I haven’t tested it.)
  • some secret sauce
  • SQL Server
  • SQL Server Reporting Services (or your favorite reporting suite)

So, what is the secret sauce?  Well, SQL Server’s ADO.NET provider exposes the SQL Server Bulk Import API through a class called SqlBulkCopy.  We can easily craft a C# program to take the clean, consistent output of Log Parser and stream it into SQL Server at breakneck speeds.

Log Parsing

First, let’s get Log Parser humming.  Microsoft’s Log Parser accepts SQL-like commands from the command line, and, depending on the particular command, can output to a variety of text formats, execute SQL statements, or create images.  Here is an example of the type of SQL Statement we are looking for:

SELECT * INTO log1.csv FROM C:\WINDOWS\system32\LogFiles\W3SVC1\ex110101.log

However, as you may be able to tell from the file path, we need to parameterize the source for different IIS sites (e.g. W3SVC9999) and dates (e.g. ex110228.log).  Here is an example command script that can take care of those variables:

@echo off
set logparser="C:\Program Files\Log Parser 2.2\LogParser.exe"

set siteid=%1
if "%siteid%"=="" set siteid=1
set logdate=%date:~-2%%date:~4,2%%date:~7,2%
%logparser% "SELECT * INTO log-w3svc%siteid%-ex%logdate%.csv FROM C:\WINDOWS\system32\LogFiles\W3SVC%siteid%\ex%logdate%.log"

If you run that, you should end up with a CSV file in the current directory, with the current day’s logs for the default IIS website.  It also accepts the website ID number as a parameter on the command line, if you want to run this for more than one site.

Now, here I would like to note that Log Parser supports a mode of operation in which it remembers where it left off in a log file, and skips there on subsequent runs.  However, I have found that, since IIS does not flush its log files and since Windows caches writes, the log will sometimes end half-way through an entry.  When a situation like this arises, Log Parser gets confused and completely fails to properly parse any further entries.  In addition, its SQL support is useful but is unable to do the incremental loads that we would like to do.

Set-up the Database

We need a place to shove that data, but we have two masters to server at this point.  On one hand, we want to transfer as little data to the database server as possible, for obvious reasons.  On the other, we don’t want to have long-running transactions against the main IIS logs table, since it is really made for OLAP and response time of queries is paramount.  This all leans toward a scheme of staging and merging data, rather than cherry-picking new rows to import.  And, even though we don’t want to transfer tons of data, we should be OK with 1-day’s-worth in a staging table.  After the data is staged, it can be incrementally moved into the main storage table to reduce the impact on the analytics.

So, we will need two (very similar) tables during our import process: the staging table, and the main storage table.  The main storage table should match the W3C Log format, like so:

CREATE TABLE [dbo].[w3clog]
(
    [RowId] bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [LogFilename] varchar(255) NOT NULL,
    [LogRow] int NOT NULL,
    [date] date NULL,
    [time] time(0) NULL,
    [datetime] AS (CONVERT(datetime2(0), date + CONVERT(datetime, time, 0), 0)) PERSISTED,
    [c-ip] varchar(50) NULL,
    [cs-username] varchar(255) NULL,
    [s-sitename] varchar(255) NULL,
    [s-computername] varchar(255) NOT NULL,
    [s-ip] varchar(50) NULL,
    [s-port] varchar(255) NULL,
    [cs-method] varchar(255) NULL,
    [cs-uri-stem] varchar(2048) NULL,
    [cs-uri-query] varchar(max) NULL,
    [sc-status] int NULL,
    [sc-substatus] int NULL,
    [sc-win32-status] bigint NULL,
    [sc-bytes] int NULL,
    [cs-bytes] int NULL,
    [time-taken] bigint NULL,
    [cs-version] int NULL,
    [cs-host] varchar(255) NULL,
    [cs(User-Agent)] varchar(1000) NULL,
    [cs(Cookie)] varchar(max) NULL,
    [cs(Referer)] varchar(2000) NULL,
    [s-event] varchar(255) NULL,
    [s-process-type] varchar(255) NULL,
    [s-user-time] int NULL,
    [s-kernel-time] int NULL,
    [s-page-faults] int NULL,
    [s-total-procs] int NULL,
    [s-active-procs] int NULL,
    [s-stopped-procs] int 
NULL
)

The staging table will be an almost identical temp table, with the single omission of the computed ‘datetime’ column.

Build the Importer

So, now it is time to explore* the power of the SqlBulkImporter class.

*This code is available on GitHub, so there is no need to follow along unless you are building a custom version.  In light of this, I will be skimming over a few of the easier parts.

Create the Connections

Our importer needs to be able to read the CSV files, and spit the data into SQL server. Luckily, both of these are supported by Microsoft’s ADO.NET providers. First, we will use a standard SQL Server connection for the destination database:

SqlBulkCopy - 1 - WebLogs Connection

Next, we will use an ODBC connection for the CSV files:

Driver={Microsoft Text Driver (*.txt; *.csv)};Dbq=.;Extensions=csv

SqlBulkCopy - 2 - CSV Connection

Using the Microsoft Text Driver, it is possible to read CSV files like this:

var table = new DataTable();
using (var connection = new OdbcConnection(Settings.Default.ImportDriver))
{
    var dataAdapter = new OdbcDataAdapter("select * from log1.csv", connection);
    dataAdapter.Fill(table);
}

Streaming the Data

The little example above is fine-and-dandy for reading the data into memory, but just for the sake of future-proofing, we should aim to stream the data in.  This will ensure that we can import successfully, even during extreme loads. Rather than using a DataAdapter and DataTable, we will opt to read the rows and columns ourselves with the ‘ExecuteReader’ family of methods.

To clear up any uncertainty, here is the order and nesting of the various operations that will need to take place for a successful, performant import:

  • Connect to the destination database
    • Begin a transaction
      • Create a temporary “staging” table for the logs
      • For each CSV file of interest:
        • Open a reader for the CSV file
          • Bulk-import the data from the CSV file into the staging table
        • Close the CSV file
      • Merge the contents of the staging table into the main storage table
    • Commit the transaction
  • Disconnect from the destination database

There are only two noteworthy bits: the bulk import itself and the merge operation.

The Bulk Import

The code I use to do the import is fairly simple:

private static void BulkImport(IDataReader reader, SqlTransaction transaction)
{
    using (var importer = new SqlBulkCopy(transaction.Connection, SqlBulkCopyOptions.Default, transaction))
    {
        for (int field = 0; field < reader.FieldCount; field++)
        {
            var name = reader.GetName(field);
            importer.ColumnMappings.Add(name, name);
        }

        importer.DestinationTableName = "#w3clog_staging";
        importer.WriteToServer(reader);
    }
}

Simple as it is, there is a lot that we can learn from this compact section of code:

  • The SqlBulkCopy class can be given options to create an internal transaction, fire (or bypass) triggers, fire (or bypass) check constraints, insert into identity columns, and etc.
  • Bulk SQL operations can enter into external transaction, and can be rolled back in the same way that any transaction can. I was expecting this, but was still pleasantly surprised to find that SQL Server inherently supports this.
  • SQL Bulk Copy operations use an ordinal column mapping by default.  This means that columns are mapped based on their position in the import data and in the table rather than their names.  This can be overridden by mapping each column pair by some combination of ordinal position and column name.  Here, we are using a simple name-to-name mapping.
  • Bulk operations can be performed against temp tables.  I was not expecting this to work, and was again pleasantly surprised.
  • The SqlBulkCopy class can use either a set of DataRows (from a DataTable, for example) or an IDataReader as its source.
  • ADO.NET does not have a ‘time’ data type, it uses DateTime instead. In addition, the bulk importer does not expose any way to manually convert the columns.  Therefore, either the destination table must use ‘datetime’ for all ‘time’ columns, or the source reader must expose the data as ‘varchar’.

The Merge Operation

The final notable part of the program is the merge operation.  There are several ways to get this done; from a single, complex insert statement, to individual insert operations. 

We will be aiming for a set-based approach, utilizing joins to do most of the heavy lifting.

First, to delete entries that already exist in the main table, we issue a command like this:

DELETE
    s
FROM
    [#w3clog_staging] s
INNER JOIN
    dbo.[w3clog] p
  ON
    s.[LogFilename] = p.[LogFilename]
  AND
    s.[LogRow] = p.[LogRow]

Simple stuff really, but to help it out, we will probably want a unique index on the main table: (this only needs to be done once)

CREATE UNIQUE NONCLUSTERED INDEX [UK_w3clog_LogRow] ON dbo.[w3clog] 
(
    [LogFilename] ASC,
    [LogRow] ASC
)

Next we need to delete duplicate entries that may have crept into the staging table:

DELETE FROM
    [#w3clog_staging]
WHERE
    [RowId] IN
      (
        SELECT
            RowId
       
FROM
         
(
            SELECT
                RowId,
                ROW_NUMBER() OVER (PARTITION BY [LogFilename], [LogRow] ORDER BY [RowId]) [Instance]
            FROM
                [#w3clog_staging]
          ) instances
        WHERE
            [Instance] > 1
      )

This simply uses the SQL Server Window Function ‘ROW_NUMBER()’ to determine individual rows to delete.

The final action is to move the data into the main table:

INSERT INTO
    [w3clog]
    (
        [LogFilename], [LogRow], [date], …
    )
SELECT
    [LogFilename], [LogRow], [date], …
FROM
    [#w3clog_staging]

Done! Now our whole import process is complete. Let’s see if we can turn this into pretty graphs…

Analyzing the Data

Let’s get a simple graph showing the last 3 day’s hits, upload, and download, grouped into hourly buckets.

First, the query:

SELECT
    DATEPART(hour, [time]),
    COUNT(*) [hits],
    SUM([sc-bytes]) [upload-bytes],
    SUM([cs-bytes]) [download-bytes]
FROM
    dbo.[w3clog]
WHERE
    [datetime] >= DATEADD(day, -3, GETUTCDATE())
GROUP BY
    DATEPART(hour, [time])

Simple stuff, right?

So, here is how my server’s graph looks: (in Excel, since I’m using SQL Express on my server)

SqlBulkCopy - 2 - Graph

Can we find out what bots are hitting the site?  Sure:

SELECT
    [cs(User-Agent)],
    COUNT(*) [hits],
    COUNT(DISTINCT [c-ip]) [ips]
FROM
    [w3clog]
WHERE
    [cs(User-Agent)] LIKE '%http%'
GROUP BY
    [cs(User-Agent)]
ORDER BY
    [hits] DESC,
    [ips] DESC

Here are my results:

User Agent Hits Distinct IPs
Mozilla/5.0+(compatible;+Yahoo!+Slurp;+http://help.yahoo.com/help/us/ysearch/slurp) 71624 112
Mozilla/5.0+(compatible;+Googlebot/2.1;++http://www.google.com/bot.html) 60536 441
Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) 32636 329
Mozilla/5.0+(compatible;+Baiduspider/2.0;++http://www.baidu.com/search/spider.html) 23749 225
Mozilla/5.0+(compatible;+YandexBot/3.0;++http://yandex.com/bots) 16696 7
webnumbrFetcher/1.0+(http://webnumbr.com/) 16551 1
Mozilla/5.0+(compatible;+DotBot/1.1;+http://www.dotnetdotcom.org/,+crawler@dotnetdotcom.org) 14147 5
Mozilla/5.0+(compatible;+Yahoo!+Slurp/3.0;+http://help.yahoo.com/help/us/ysearch/slurp) 13892 61
Baiduspider+(+http://www.baidu.com/search/spider.htm) 10445 550
Sosospider+(+http://help.soso.com/webspider.htm) 8280 76
Mozilla/5.0+(compatible;+SiteBot/0.1;++http://www.sitebot.org/robot/) 7614 5
Mozilla/5.0+(compatible;+MJ12bot/v1.3.3;+http://www.majestic12.co.uk/bot.php?+) 7085 241

Interesting stuff!

I have included many more queries to play with in the project on GitHub.  Stay tuned for more!


 
Categories: iis | sql

 

Warning: the APIs used in this demo are subject to improvement in the future.  This is just a demo to get your feet wet with the 0.2.0.1 release of IronJS.  IronJS is still in the early stages, so your mileage may vary.

How would you like to have a CoffeeScript compiler, running 100% managed code?  Well, I’ll leave out the foreplay and get right to it.

Fire up an instance of Visual Studio, with NuGet installed.

This is pretty self-explanatory, but just in case you don’t have NuGet installed, you can follow the directions on the NuGet website to get started.

Create a new Windows Application.

Create a new Windows Application and name it ‘CoffeeDemo’.

Use NuGet to install IronJS.

For those of you experienced with NuGet, the command in the Package Manager Console is:

Install-Package IronJS

If you prefer to use the GUI to add your package, you can consult the GUI directions provided at NuGet.org

Lay-out your project and form.

At a minimum, you should add the following to your form:

  1. An input textbox, for CoffeeScript.
  2. An output textbox, for JavaScript.
  3. A button, for executing the JavaScript.

You will want to make your text boxes Multiline, and large enough to enter some JavaScript source

Here is how mine looks:
CoffeeDemo - 1 - Layout

I decided to use a split panel (anchored to all four sides) to contain my text boxes.  I set my text boxes to dock-fill and gave them both horizontal and vertical scroll bars.  I also anchored my Execute button to the bottom right.  Finally, I hate the name Form1, so I always rename my form to View or MainView.

Name your controls CoffeeScriptBox, JavaScriptBox, and ExecuteButton.

Create the IronJS context.

In the code for our view, we need to create an IronJS context, to do all of the heavy lifting behind the scenes.  We will do this with a function InitializeContext, called from the constructor:

using System;
using System.Windows.Forms;
using IronJS;
using IronJS.Hosting;
using IronJS.Native;

namespace CoffeeDemo
{
    public partial class View : Form
    {
        private CSharp.Context context;

        public View()
        {
            InitializeContext();
            InitializeComponent();
        }

        private void InitializeContext()
        {
            Action<string> alert = message => MessageBox.Show(message);

            this.context = new CSharp.Context();
            this.context.SetGlobal("alert", Utils.CreateFunction(this.context.Environment, 1, alert));
        }
    }
}

Here, we can see the basics of creating a contexts, as well as exposing a CLR function to JavaScript.  The alert function is now wired up to call directly into MessageBox.Show.

Wire-up the execute button.

Just wire up the Click even of the execute button to a function like thisk,:

private void ExecuteButton_Click(object sender, EventArgs e)
{
    try
    {
        this.context.Execute(this.JavaScriptBox.Text);
    }
    catch (IronJS.Error.Error ex)
    {
        MessageBox.Show(ex.ToString(), "Error from IronJS");
    }
}

You can run a test like this:
CoffeeDemo - 2 - Hello World

Automatically download CoffeeScript.

There are a couple of different methods that we could use to bring CoffeeScript into our app.  We could include it in the folder, we could add it as an assembly resource, etc.

My preference for this demo, however, is to download CoffeeScript to the /bin directory on the first run of the app.


using System.IO;
using System.Net;


namespace CoffeeDemo
{
    public partial class View : Form
    {

        public View()
        {
           

            LoadCoffeeScript();
        }



        private void LoadCoffeeScript()
        {
            if (!File.Exists("coffee-script.js"))
            {
                var client = new WebClient();
                client.DownloadFile("https://raw.github.com/jashkenas/coffee-script/master/extras/coffee-script.js", "coffee-script.js");
            }

            this.context.ExecuteFile("coffee-script.js");
        }
    }
}

At this point CoffeeScript is ready to go, and can be tested in the JavaScript text box.  We are loading it in the constructor, which could negatively impact the application start-up time.  However, for this example, I’m not concentrating on performance.

Try this for an example:

alert(CoffeeScript.compile('a = (x) -> x * x', { bare: true }));

Wire-up the TextChanged event for live updating.

Add a field of type FunctionObject to the view’s class, and add this function as the TextChanged event on the CoffeeScript text box:

private FunctionObject compile;
...

private void CoffeeScriptBox_TextChanged(object sender, EventArgs e)
{
    if (this.compile == null)
    {
        this.context.Execute("var compile = function (src) { return CoffeeScript.compile(src, { bare: true }); };");
        this.compile = this.context.GetGlobalAs<FunctionObject>("compile");
    }

    try     {         var boxedResult = this.compile.Call(this.context.Globals, this.CoffeeScriptBox.Text);         var result = TypeConverter.ToString(boxedResult);         this.JavaScriptBox.Text = result.Replace("\n""\r\n");     }     catch (IronJS.Error.Error)     {     }
}

This shows you how to call a JavaScript function from the CLR.

CoffeeDemo in action!

CoffeeDemo - 3 - Epic Win!

You should now have a working, live-updating CoffeeScript compiler.  You will almost certainly notice the compilation lag on the first key-press where we are compiling the helper function, and calling the CoffeeScript compiler for the first time.  The lag is due to IronJS pushing everything into an in-memory, dynamic assembly.  Subsequent key-presses should be pretty quick, tho, and we are striving to make it faster.

Stay tuned.

We are actively working on better .NET integration so that you can use native .NET types, all without using wrapper functions.  So, stay with us, and please follow us on Github!


 
Categories: dlr | ironjs | javascript

The Problem

If you are like me, you have probably had trouble in the past trying to get JavaScript IntelliSense to work well in Visual Studio.

You have probably gone through these steps:

“I’ll just add my script here, and presto!”
AddScript

“Well, that didn’t work.  It kept the App Path Modifier… dammit.”
Result

“I’ll just add a run-at attribute, so that ASP.NET transforms the path for me…”
RunAtServer

“Oh, my goodness gracious!”
RunAtServerResult

So where does that leave us?

We can’t use runat="server" on script tags, because ASP.NET has repurposed this format in order to include server-side code into the page.  This in effect means that we can’t use the “virtual path” to reference the same script from everywhere in our app.  Since we can’t use a virtual path, and we can’t use a relative path, we are stuck.

Add on top of this that we often need to switch between different scripts for auto-completion, debugging, and production and you start to see the direness of the situation.

At the center of the issue is the fact that Visual Studio can’t execute code in the editor in order to “know” where a script will come from.  If it could, then we would just write a chunk of code to do the virtual path transformation based on current mode.  We would have minified scripts for production (by default), VSDoc scripts for IntelliSense (in design mode), and full source scripts for debugging (when DEBUG is defined or a debugger is attached).

At this point, I used to conclude that I had to live with all of my .aspx files living in one folder, or I had to live without JavaScript IntelliSense.

A Solution

The little bit of magic required to make everything work in our favor is the fact that Visual Studio will treat everything outside of <% ... %> code sections as though it were one continuous block of HTML.  This means that we can use a technique similar to conditional comments in order to ignore sections of HTML, but still have it recognized as part of the HTML document.

Take these lines for example:

<% if (false) { %>
Test
<% } %>

After being transformed by ASP.NET into a pure C# listing, this code will be compiled into an assembly.  Since the code is unreachable, it will be ignored and will not be checked for definite assignment as per the C# spec (§5.3.3.1).

Extending this idea a little bit further, we can add our long-sought-after IntelliSense:

<% if (false) { %>
<script src="../../Scripts/jquery-1.4.1-vsdoc.js" type="text/javascript"></script>
<% } %>

We can use the relative path here, since this will never make it across the wire.  Just make sure that the script is referenced relative to the physical file in which this tag exists.

Now, for the real script tag, we can use the static functions that ASP.NET provides to emit the proper path:

<script src="<%: Url.Content("~/Scripts/jquery-1.4.1-vsdoc.js") %>" type="text/javascript"></script>

For ASP.NET WebForms, you would use the Server.ApplyAppPathModifier() function in place of Url.Content().

Variations and Conclusion

Variations on this trick would include using compiler directives like “#if FALSE”, or /* block comments */ to exclude the section of code.  Both of these methods seem to work.

If you use the method described above with a build server, or while compiling view locally, you may get a compiler warning saying “unreachable code detected.”  If this warning bothers you you can use “#pragma warning disable 162” and “#pragma warning enable 162” at the start and end of your if statement to suppress it.

I hope this helps you get the most out of Visual Studio and ASP.NET!  Your feedback is welcomed.


 
December 23, 2010
@ 01:23 AM
Regenerate your T4 files (including T4MVC) on every build.
 
Categories: asp.net | asp.net-mvc | msbuild | t4 | visual-studio