You are currently browsing the category archive for the ‘ASP.NET’ category.

Here’s a simple view engine for ASP.NET MVC that lets you use plain HTML for your views, even if it’s badly formed!  It supports a very simple attribute syntax for embedding other partial views in the page; those views can use whichever view engine you’d like (WebForms, Razor, NHaml etc).

Let’s start with the composite view engine; its job is to find a container view based on the Master page name, but also find a primary partial view based on the current MVC controller and action:

public interface ICompositeView
{
    IView PrimaryView { set; }
}

public abstract class CompositeViewEngine : VirtualPathProviderViewEngine
{
    private ViewEngineCollection otherViewEngines;

    public CompositeViewEngine()
    {
    }

    public override ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName, bool useCache )
    {
        if ( !controllerContext.IsChildAction )
        {
            var result = base.FindView( controllerContext, GetMasterName( controllerContext.RouteData.Values, masterName ), null, useCache );
            var compositeView = result.View as ICompositeView;

            if ( compositeView != null )
            {
                compositeView.PrimaryView = OtherViewEngines.FindPartialView( controllerContext, viewName ).View;

                return result;
            }
        }
        else
        {
            return OtherViewEngines.FindView( controllerContext, viewName, null );
        }

        return new ViewEngineResult( Enumerable.Empty<string>() );
    }

    public override ViewEngineResult FindPartialView( ControllerContext controllerContext, string partialViewName, bool useCache )
    {
        return new ViewEngineResult( Enumerable.Empty<string>() );
    }

    private ViewEngineCollection OtherViewEngines
    {
        get
        {
            lock ( this )
            {
                return ( otherViewEngines != null )
                    ? otherViewEngines
                    : otherViewEngines = new ViewEngineCollection( ViewEngines.Engines.Where( e => !( e is CompositeViewEngine ) ).ToList() );
            }
        }
    }

    protected virtual string GetMasterName( RouteValueDictionary routeValues, string defaultName )
    {
        return defaultName;
    }
}

After finding its own container view, it gives all other view engines the opportunity to find the contained partial.  Here’s the HTML view engine that derives from it:

public class HtmlViewEngine : CompositeViewEngine
{
    public IHtmlViewHelper Helper { get; set; }

    public HtmlViewEngine()
    {
        this.AreaViewLocationFormats = new string[]
        {
            "~/Areas/{2}/Views/{1}/{0}.html",
            "~/Areas/{2}/Views/{1}/{0}.htm",
            "~/Areas/{2}/Views/Shared/{0}.html",
            "~/Areas/{2}/Views/Shared/{0}.htm"
        };

        this.ViewLocationFormats = new string[]
        {
            "~/Views/{1}/{0}.html",
            "~/Views/{1}/{0}.htm",
            "~/Views/Shared/{0}.html",
            "~/Views/Shared/{0}.htm"
        };

        this.FileExtensions = new string[]
        {
            "html",
            "htm"
        };
    }

    protected override string GetMasterName( RouteValueDictionary routeValues, string defaultName )
    {
        return routeValues.ContainsKey( "path" )
            ? ( (string)routeValues[ "path" ] ).Split( '.' ).First()
            : !string.IsNullOrEmpty( defaultName ) ? defaultName : "Index";
    }

    protected override IView CreateView( ControllerContext controllerContext, string viewPath, string masterPath )
    {
        return new HtmlView( viewPath, Helper );
    }

    protected override IView CreatePartialView( ControllerContext controllerContext, string partialPath )
    {
        return new HtmlView( partialPath, Helper );
    }
}

Once the container view is found, it creates an HtmlView that knows how to render it.  Here’s how that looks (starting with a CompositeView):

public abstract class CompositeView : IView, ICompositeView
{
    protected string filename;

    public IView PrimaryView { get; set; }

    public CompositeView( string filename )
    {
        this.filename = filename;
    }

    public abstract void Render( ViewContext viewContext, TextWriter writer );
}
public interface IHtmlViewHelper
{
    void RenderContent( HtmlDocument document, ViewRenderer renderer );
}

public class HtmlView : CompositeView
{
    protected IHtmlViewHelper helper;
    protected HtmlDocument source;

    public HtmlView( string filename, IHtmlViewHelper helper )
        : base( filename )
    {
        this.helper = helper;
    }

    public override void Render( ViewContext viewContext, TextWriter writer )
    {
        var document = GetSource( viewContext );

        if ( helper != null )
        {
            var viewDataContainer = new ViewDataContainer( viewContext.ViewData.Model );
            var htmlHelper = new HtmlHelper( viewContext, viewDataContainer );

            helper.RenderContent( document, new ViewRenderer( viewContext, htmlHelper, PrimaryView ) );
        }

        document.Save( writer );
    }

    private HtmlDocument GetSource( ControllerContext controllerContext )
    {
        return source ?? ( source = GetSource( controllerContext.HttpContext, filename ) );
    }

    private HtmlDocument GetSource( HttpContextBase httpContext, string filename )
    {
        return httpContext.RequestCache().Cache( filename, () => LoadSource( httpContext, filename ) );
    }

    private HtmlDocument LoadSource( HttpContextBase httpContext, string filename )
    {
        var doc = new HtmlDocument();

        doc.Load( httpContext.Server.MapPath( filename ) );

        return doc;
    }
}

It uses HtmlAgilityPack to parse the HTML (with a little caching), injects new content into the DOM, then renders the result with some help from the ViewRenderer class (catches & pretty prints any rendering errors too):

public class ViewRenderer
{
    private ViewContext viewContext;
    private HtmlHelper htmlHelper;
    private IView primaryView;
    private ViewEngineCollection otherViewEngines;

    public ViewRenderer( ViewContext viewContext, HtmlHelper htmlHelper, IView primaryView )
    {
        this.viewContext = viewContext;
        this.htmlHelper = htmlHelper;
        this.primaryView = primaryView;
        this.otherViewEngines = new ViewEngineCollection( ViewEngines.Engines.Where( e => !( e is CompositeViewEngine ) ).ToList() );
    }

    public MvcHtmlString RenderContent( bool usePrimaryView, string actionName = null, string controllerName = null, string viewName = null )
    {
        var rendered = ( viewName != null )
            ? RenderView( viewName )
            : null;

        if ( rendered == null && usePrimaryView && ( controllerName == null || controllerName == (string)viewContext.RouteData.Values[ "controller" ] ) )
        {
            rendered = RenderView( primaryView );
        }

        if ( rendered == null ) rendered = RenderAction( actionName ?? "Index", controllerName );

        return rendered ?? MvcHtmlString.Empty;
    }

    public MvcHtmlString RenderView( string viewName )
    {
        return RenderView( FindView( viewName ) );
    }

    public MvcHtmlString RenderAction( string actionName, string controllerName = null )
    {
        MvcHtmlString result = null;

        try
        {
            result = htmlHelper.Action( actionName, controllerName );
        }
        catch ( HttpException ex )
        {
            result = MvcHtmlString.Create( ex.GetHtmlErrorMessage() ?? new HttpUnhandledException( ex.Message, ex.InnerException ).GetHtmlErrorMessage() );
        }
        catch ( Exception ex )
        {
            result = MvcHtmlString.Create( new HttpUnhandledException( ex.Message ).GetHtmlErrorMessage() );
        }

        return result;
    }

    private IView FindView( string viewName )
    {
        var result = otherViewEngines.FindPartialView( viewContext, viewName );

        return ( result.View != null ) ? result.View : null;
    }

    private MvcHtmlString RenderView( IView view )
    {
        if ( view == null ) return null;

        using ( var writer = new StringWriter() )
        {
            var renderViewContext = new ViewContext( viewContext, view, viewContext.ViewData, viewContext.TempData, writer );

            try
            {
                view.Render( renderViewContext, writer );
            }
            catch ( HttpException ex )
            {
                writer.Write( ex.GetHtmlErrorMessage() ?? new HttpUnhandledException( ex.Message, ex.InnerException ).GetHtmlErrorMessage() );
            }
            catch ( Exception ex )
            {
                writer.Write( new HttpUnhandledException( ex.Message ).GetHtmlErrorMessage() );
            }

            return MvcHtmlString.Create( writer.ToString() );
        }
    }
}

Finally we need to tell MVC about the view engine. Similar to the RouteConfig class you’ll see in a new MVC 4 project, here’s ViewEngineConfig:

public class ViewEngineConfig
{
    public static void RegisterEngines( ViewEngineCollection viewEngines )
    {
        viewEngines.Insert( 0, new HtmlViewEngine()
        {
            Helper = new HtmlViewHelper()
        } );
    }

    private class HtmlViewHelper : IHtmlViewHelper
    {
        public void RenderContent( HtmlDocument document, ViewRenderer renderer )
        {
            foreach ( var node in SelectNodes( document.DocumentNode, "//*[@html-primary or @html-controller or @html-action]" ) )
            {
                var isPrimary = node.GetAttributeValue( "html-primary", false );
                var controllerName = node.GetAttributeValue( "html-controller", null );
                var actionName = node.GetAttributeValue( "html-action", null );

                node.InnerHtml = renderer.RenderContent( isPrimary, actionName, controllerName ).ToHtmlString();
            }

            foreach ( var node in SelectNodes( document.DocumentNode, "//*[@html-partial]" ) )
            {
                node.InnerHtml = ( renderer.RenderView( node.Attributes[ "html-partial" ].Value ) ?? MvcHtmlString.Empty ).ToHtmlString();
            }
        }

        public string GetControllerName( HtmlDocument document )
        {
            var controllerNode = document.DocumentNode.SelectSingleNode( "//*[@html-controller]" );

            return ( controllerNode != null ) ? controllerNode.GetAttributeValue( "html-controller", null ) : null;
        }

        private static IEnumerable<HtmlNode> SelectNodes( HtmlNode node, string xpath )
        {
            return node.SelectNodes( xpath ) ?? Enumerable.Empty<HtmlNode>();
        }
    }
}

This is doing most of the content substitution.  It’s looking for a few pre-defined attributes in the HTML (html-primary, html-partial, html-controller and html-action) and replacing the content as needed.

Call RegisterEngines in Application_Start (in Global.asax.cs) and you’re done:

ViewEngineConfig.RegisterEngines( ViewEngines.Engines );

This works great when you’re just using regular MVC controller / action routes, but what if you want to handle direct requests for the HTML views? (for example if you’re hosting an entire static site within your MVC project… not as odd as it might sound).  We can do this by adding some Route definitions:

// Controller prefixed resources
routes.Add( "ControllerStaticResource", new Route( @"{controller}/{*path}", new StaticFileRouteHandler() )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(css|js|png|jpg|gif)" } ),
    Defaults = new RouteValueDictionary( new { rootFolder = "~/Views", folder = "Shared" } ),
} );

// Resources
routes.Add( "StaticResource", new Route( @"{*path}", new StaticFileRouteHandler() )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(css|js|png|jpg|gif)" } ),
    Defaults = new RouteValueDictionary( new { rootFolder = "~/Views", folder = "Shared" } )
} );

// Static HTML path with controller and action prefix
routes.Add( "ControllerActionStaticHtml", new PlaceholderRoute( @"{controller}/{action}/{*path}", handler )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(html|htm)" } ),
    Excludes = new[] { "path" }
} );

// Static HTML path with controller prefix
routes.Add( "ControllerStaticHtml", new PlaceholderRoute( @"{controller}/{*path}", handler )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(html|htm)" } ),
    Defaults = new RouteValueDictionary( new { controller = "Home", action = "Index", path = UrlParameter.Optional } ),
    Excludes = new[] { "path" }
} );

// Static HTML path with controller prefix
routes.Add( "StaticHtml", new PlaceholderRoute( @"{*path}", handler )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(html|htm)" } ),
    Defaults = new RouteValueDictionary( new { controller = "Home", action = "Index", path = UrlParameter.Optional } ),
    Excludes = new[] { "path" }
} );

// Static HTML path with controller and action prefix
routes.Add( "ControllerActionStaticHtmlGenerate", new PlaceholderRoute( @"{controller}/{action}/{path}", handler )
{
    Defaults = new RouteValueDictionary( new { controller = "Home", action = UrlParameter.Optional, path = UrlParameter.Optional } ),
    Placeholders = new RouteValueDictionary( new { action = "Index" } ),
    Excludes = new[] { "path" }
} );

This could be more complex than you need, so don’t freak out just yet Smile  The first couple are intercepting css, js, png etc files, and pointing them to a new StaticFileRouteHandler class.  Next we’re looking for .html and .html files, but letting the regular MvcRouteHandler take care of those.

Browsers expect non-absolute resource paths to be relative to the current request URL.  In this example we have CSS files, images etc in subfolders of the Views/Shared folder, along with the composite HTML files.  However the URL the browser sees might be just an MVC path.  The StaticFileRouteHandler lets us intercept those resource requests and grab the files from the appropriate place.

This isn’t a requirement (resources could be in the typical ~/Content folder if you prefer) but it can be pretty convenient. By keeping the embedded site files together they can be modified with any HTML editor. If a third party is responsible for those, you can just drop in the entire site when they make changes.

Here’s StaticFileRouteHandler:

public class StaticFileRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler( RequestContext requestContext )
    {
        return new StaticFileHttpHandler( requestContext );
    }

    public class StaticFileHttpHandler : IHttpAsyncHandler, IHttpHandler //, IRequiresSessionState
    {
        private delegate void AsyncProcessorDelegate( HttpContext httpContext );

        protected RequestContext requestContext;
        private AsyncProcessorDelegate asyncDelegate;

        public StaticFileHttpHandler( RequestContext requestContext )
        {
            this.requestContext = requestContext;
        }

        public void ProcessRequest( HttpContext context )
        {
            var routeValues = requestContext.RouteData.Values;

            var controllerName = (string)routeValues[ "controller" ];
            var folderName = (string)routeValues[ "folder" ];
            var path = GetFilePath( context );

            var filePath = ( controllerName != null )
                ? FindFilePath( controllerName, path ) ?? FindFilePath( folderName, path )
                : FindFilePath( folderName, path );

            if ( filePath != null )
            {
                var response = context.Response;

                response.ContentType = GetContentType( filePath );
                response.AddFileDependency( filePath );
                response.Cache.SetETagFromFileDependencies();
                response.Cache.SetLastModifiedFromFileDependencies();
                response.Cache.SetCacheability( HttpCacheability.Public );

                context.Response.TransmitFile( filePath );
            }
            else
            {
                System.Diagnostics.Trace.WriteLine( string.Format( "ERROR: StaticRouteHandler couldn't find {0}", context.Request.Url ) );
                context.Response.StatusCode = 404;
            }
        }

        private string GetFilePath( HttpContext context )
        {
            var routeValues = requestContext.RouteData.Values;

            if ( context.Request.UrlReferrer == null ) return (string)routeValues[ "path" ];

            var urlBase = "http://" + context.Request.Url.GetComponents( UriComponents.Host | UriComponents.Path, UriFormat.Unescaped );
            var referrerBase = "http://" + context.Request.UrlReferrer.GetComponents( UriComponents.Host | UriComponents.Path, UriFormat.Unescaped );

            var url = new Uri( urlBase, UriKind.Absolute );
            var referrer = new Uri( referrerBase, UriKind.Absolute );

            return referrer.MakeRelativeUri( url ).OriginalString;
        }

        private string FindFilePath( string folderName, string path )
        {
            var httpContext = requestContext.HttpContext;
            var routeValues = requestContext.RouteData.Values;

            var filePath = string.Format( "{0}/{1}/{2}",
                routeValues[ "rootFolder" ],
                folderName,
                path );

            var absolutePath = httpContext.Server.MapPath( filePath );

            System.Diagnostics.Trace.WriteLine( string.Format( "Looking for file in {0}", absolutePath ) );

            return File.Exists( absolutePath ) ? absolutePath : null;
        }

        private string GetContentType( string filePath )
        {
            var extension = System.IO.Path.GetExtension( filePath );

            switch ( extension )
            {
                case ".htm":
                case ".html": return "text/html";
                case ".css": return "text/css";
                case ".js": return "application/javascript";
                case ".png": return "image/png";
                case ".jpg": return "image/jpeg";
                case ".gif": return "image/gif";
            }

            return "text/plain";
        }

        public IAsyncResult BeginProcessRequest( HttpContext context, AsyncCallback cb, object extraData )
        {
            asyncDelegate = ProcessRequest;

            return asyncDelegate.BeginInvoke( context, cb, extraData );
        }

        public void EndProcessRequest( IAsyncResult result )
        {
            asyncDelegate.EndInvoke( result );
        }

        public bool IsReusable
        {
            get { return true; }
        }
    }
}

Full source and sample project coming soon! Smile

Advertisements

ASP.NET MVC’s inbuilt Route class can handle just about anything you want.  However if you need a bit more control it’s easy to derive your own.  Here’s a simple class called DirectionalRoute that adds a couple of features:

  • CanGetRouteData: Set if this route can be used to parse a URL
  • CanGetVirtualPath: Set if this route can be used to generate a URL
  • Placeholders: Dictionary of placeholder values
  • Excludes: Array of keys to exclude from generated URL

The Placeholders dictionary needs a little explanation.  Setting Default values on a regular route works well, but if your RouteData contains a value that matches a default (or if the default is set to UrlParameter.Optional), it could be excluded completely from generated URLs. In situations where you really need a value to be present, set a Placeholder.

Here’s the code:

public class DirectionalRoute : Route
{
    public bool CanGetRouteData { get; set; }
    public bool CanGetVirtualPath { get; set; }
    public RouteValueDictionary Placeholders { get; set; }
    public string[] Excludes { get; set; }

    public DirectionalRoute(string url, IRouteHandler routeHandler)
        : this( url, null, null, null, routeHandler )
    {
    }

    public DirectionalRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
        : this( url, defaults, null, null, routeHandler )
    {
    }

    public DirectionalRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
        : this( url, defaults, constraints, null, routeHandler )
    {
    }

    public DirectionalRoute( string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler )
        : base( url, defaults, constraints, dataTokens, routeHandler )
    {
        this.CanGetRouteData = true;
        this.CanGetVirtualPath = true;
    }

    public override RouteData GetRouteData( System.Web.HttpContextBase httpContext )
    {
        if ( !CanGetRouteData ) return null;

        var routeData = base.GetRouteData( httpContext );

        if ( routeData != null && Placeholders != null )
        {
            var missing = routeData.Values
                .Where( rv => ( rv.Value == null || rv.Value == UrlParameter.Optional ) && Placeholders.ContainsKey( rv.Key ) )
                .ToArray();

            foreach ( var m in missing ) routeData.Values[ m.Key ] = Placeholders[ m.Key ];
        }

        return routeData;
    }

    public override VirtualPathData GetVirtualPath( RequestContext requestContext, RouteValueDictionary values )
    {
        return CanGetVirtualPath
            ? base.GetVirtualPath( GetRequestContext( requestContext ), GetRouteValues( values ) )
            : null;
    }

    private RequestContext GetRequestContext( RequestContext requestContext )
    {
        if ( Excludes == null || Excludes.Length == 0 ) return requestContext;

        var newRouteData = new RouteData( requestContext.RouteData.Route, requestContext.RouteData.RouteHandler );

        foreach ( var v in requestContext.RouteData.Values.Where( v => !Excludes.Contains( v.Key ) ) ) newRouteData.Values[ v.Key ] = v.Value;
        foreach ( var v in requestContext.RouteData.DataTokens ) newRouteData.DataTokens[ v.Key ] = v.Value;

        return new RequestContext( requestContext.HttpContext, newRouteData );
    }

    private RouteValueDictionary GetRouteValues( RouteValueDictionary values )
    {
        if ( Excludes == null || Excludes.Length == 0 ) return values;

        return new RouteValueDictionary( values.Where( v => !Excludes.Contains( v.Key ) ).ToDictionary( v => v.Key, v => v.Value ) );
    }
}

UPDATE: Included simple SiteMap support in sample here.

If you’ve played with the new ASP.NET routing stuff with WebForms, you probably encountered some issues with relative paths to static content (css, Javascript files etc).  This is a common issue across browsers because of the way they resolve relative paths.  Here’s an example:

    <script src="Javascript/TestScript.js" type="text/javascript"></script>

 

Let’s say this was in a page called Index.html.  Your browser request might look like this:

http://www.chriscavanagh.com/Chris/SimpleRoutingTest/Index.html

After grabbing the page content, the browser would see the relative path to TestScript.js and (reasonably) assume it could retrieve it from here:

http://www.chriscavanagh.com/Chris/SimpleRoutingTest/Javascript/TestScript.js

However, in our case we’ve thrown some funky URL routing into the mix.  Here’s how we want to hit our page:

http://www.chriscavanagh.com/Chris/SimpleRoutingTest/Search/42

Assume for now we’ve defined a Route to divert that to Index.html (in Global.asax):

routes.Add( new Route( "Search/{category}", new WebFormRouteHandler( "~/Index.html" ) ) );

 

The routing works as expected, but when the browser tries to get TestScript.js, here’s where it’ll go looking:

http://www.chriscavanagh.com/Chris/SimpleRoutingTest/Search/Javascript/TestScript.js

Fortunately there are some simple ways to fix this 🙂 (note you may need to adapt these slightly to your specific case):

  • Use ASP.NET themes and keep Javascript in embedded resources.  The theme mechanism takes care of paths nicely; it knows how to find everything from the application root and "just works".  Make Javascript files embedded resources and use ScriptManager.RegisterClientScriptResource to serve them up automatically through ScriptManager.axd.
  • Use the <base> element in your page and set its href to an absolute URL.  This can go in each page, or just in you Master page(s) if you use them.  You could add code similar to this:
    public string BaseUrl
    {
        get { return Request.Url.GetLeftPart( UriPartial.Authority ) + VirtualPathUtility.ToAbsolute( "~/" ); }
    }
    
     
  • Then make your <base> element look like this:
    <base href="<%= BaseUrl %>" />
    

    That should give you an absolute URL to your application root.  As long as all paths are relative to that, everything should work nicely 🙂

    I’ve updated my "Simple Routing Test" site to show this in action (source available here).  In this case the BaseUrl helper (above) is in the RoutablePage class.  Each page shows a bunch of links (dynamically generated against the defined routes).  At the bottom you should see "Hello from Javascript" with a yellow background.  The yellow signifies the CSS is working, and the text shows it’s successfully invoked a Javascript function 🙂

Hope this helps! 🙂

Apps

My Cashflow - The easy way to forecast your bank balances.

My Cashflow - for iPhone, iPod Touch and iPad