Archive for January, 2010

ASP.NET MVC View Engine That Supports View Path Inheritance

Tuesday, January 19th, 2010

I was working on a small MVC project where we were dealing with Inherited controllers (SomeController was inherited by SomeMoreSpecificController) and we decided that it’d be nice to have a similar hierarchy of sharing and inheritance at the View level.

Unfortunately, out of the box, ASP.net MVC looks in two default locations for your views and partials by convention.  The first is /Views/ControllerName/ViewName.aspx, the second is /Views/Shared/ViewName.aspx.  We wanted to allow SomeController to have it’s own set of more generic views, that could later be overridden in special cases by the views provided by SomeMoreSpecificController.

In order to do this in ASP.net MVC, you need to override the default view engine to change the location that the runtime looks for your views.

“Here’s one I made earlier”.

Using this ViewEngine, if you call an action method on SomeMoreSpecificController it’ll first check /Views/SomeMoreSpecificController/…, then /Views/SomeController/… (the base class), then finally /Views/Shared, allowing you a little more control over the organisation of your views.

using System.Collections.Generic;
using System.Web.Mvc;
using System;
using System.Globalization;
using System.Linq;

namespace MyMvc.Mvc
{

    public class InheranceViewEngine : WebFormViewEngine
    {
        private const string CacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:";
        private const string CacheKeyPrefixMaster = "Master";
        private const string CacheKeyPrefixView = "View";
        private static readonly List<string> EmptyLocations= new List<string>();

        public InheranceViewEngine()
        {
            ViewLocationFormats = new[]
                                  {
                                      "~/Views/{1}/{0}.aspx", "~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.aspx",
                                      "~/Views/Shared/{0}.ascx"
                                  };
        }

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }
            if (String.IsNullOrEmpty(viewName))
            {
                throw new ArgumentException("viewName");
            }

            List<string> viewLocationsSearched;
            List<string> masterLocationsSearched;

            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            string viewPath = GetPath(controllerContext, ViewLocationFormats, viewName, controllerName, CacheKeyPrefixView, useCache, out viewLocationsSearched);
            string masterPath = GetPath(controllerContext, MasterLocationFormats, masterName, controllerName, CacheKeyPrefixMaster, useCache, out masterLocationsSearched);

            if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
            {
                return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
            }

            return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
        }

        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            List<string> strArray;
            if (controllerContext == null) { throw new ArgumentNullException("controllerContext"); }
            if (string.IsNullOrEmpty(partialViewName)) { throw new ArgumentException("Partial View Name is null or empty.", "partialViewName"); }
            string requiredString = controllerContext.RouteData.GetRequiredString("controller");
            string str2 = GetPath(controllerContext, PartialViewLocationFormats, partialViewName, requiredString, "Partial", useCache, out strArray);
            if (string.IsNullOrEmpty(str2))
            {
                return new ViewEngineResult(strArray);
            }

            return new ViewEngineResult(CreatePartialView(controllerContext, str2), this);
        }

        private string GetPath(ControllerContext controllerContext, string[] locations, string name, string controllerName, string cacheKeyPrefix, bool useCache, out List<string> searchedLocations)
        {

            searchedLocations = EmptyLocations;

            if (String.IsNullOrEmpty(name))
            {
                return String.Empty;
            }

            if (locations == null || locations.Length == 0)
            {
                throw new InvalidOperationException();
            }

            bool nameRepresentsPath = IsSpecificPath(name);
            string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName);

            if (useCache)
            {
                string result = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, cacheKey);

                if (result != null)
                {
                    return result;
                }
            }

            if (nameRepresentsPath)
            {
                return GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations);
            }
            return GetPathFromGeneralName(controllerContext, locations, name, controllerName, cacheKey, ref searchedLocations);
        }

        private string GetPathFromGeneralName(ControllerContext controllerContext, string[] locations, string name, string controllerName, string cacheKey, ref List<string> searchedLocations)
        {
            string result = String.Empty;
            searchedLocations = new List<string>();

            for (int i = 0; i < locations.Length; i++)
            {
                string virtualPath = String.Format(CultureInfo.InvariantCulture, locations[i], name, controllerName);
                if (FileExists(controllerContext, virtualPath))
                {
                    searchedLocations = EmptyLocations;
                    result = virtualPath;
                    ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
                    return result;
                }
                searchedLocations.Add(virtualPath);
            }

            return GetPathFromGeneralNameOfBaseTypes(controllerContext.Controller.GetType(), locations, name, controllerContext, cacheKey, result, ref searchedLocations);
        }

        private string GetPathFromGeneralNameOfBaseTypes(Type descendantType, string[] locations, string name, ControllerContext controllerContext, string cacheKey, string result, ref List<string> searchedLocations)
        {
            Type baseControllerType = descendantType;
            if (baseControllerType == null
                || !baseControllerType.Name.Contains("Controller")
                || baseControllerType.Name == "Controller")
            {
                return result;
            }

            for (int i = 0;i < locations.Length;i++)
            {
                string baseControllerName = baseControllerType.Name.Replace("Controller", "");
                string virtualPath = String.Format(CultureInfo.InvariantCulture, locations[i], name, baseControllerName);

                if (!string.IsNullOrEmpty(virtualPath) &&
                    FileExists(controllerContext, virtualPath))
                {
                    searchedLocations = EmptyLocations;
                    result = virtualPath;
                    ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
                    return result;

                }

                searchedLocations.Add(virtualPath);
            }

            return GetPathFromGeneralNameOfBaseTypes(baseControllerType.BaseType, locations, name, controllerContext,
                                                     cacheKey, result, ref searchedLocations);
        }

        private string CreateCacheKey(string prefix, string name, string controllerName)
        {
            return String.Format(CultureInfo.InvariantCulture, CacheKeyFormat,
                                 GetType().AssemblyQualifiedName, prefix, name, controllerName);
        }

        private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref List<string> searchedLocations)
        {
            string result = name;

            if (!FileExists(controllerContext, name))
            {
                result = String.Empty;
                searchedLocations = new List<string>{ name };
            }

            ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
            return result;
        }

        private static bool IsSpecificPath(string name)
        {
            char c = name[0];
            return (c == ‘~’ || c == ‘/’);
        }

    }
}

I don’t understand Bayonetta

Sunday, January 10th, 2010

I keep seeing glowing reviews of Bayonetta.  You might have seen it advertised, the game with the “witch” that looks like Sarah Palin, who uses her hair as both a weapon and her outfit, has guns on the heels of her shoes and features in a game that has a button for “dance / taunt”.  This game has unanimously been receiving 100% or near reviews from practically every major game publication.

But you know what, I really don’t understand this game.  For a little context, I play everything.  The well reviewed stuff when it first releases, and then the mediocre games when the price drops to around £20.

But I’m just not interested in this game.  The pre release hype is tacky, it looks terrible (I don’t mean the style, the style is reasonable but playing the demo, it looks noticeably rough around the edges) and the game play appears to be extremely repetitive.

I downloaded the demo and just didn’t quite get to it until the game released, gave it a shot and was mildly entertained at best, and bored with it’s "not retro, just not sophisticated" game play within minutes.

I just don’t understand the heaps of praise people are giving this game.  Sex sells, and that aside (because actually the sexuality in the game is very comic-book like and gets annoying fast, like, during the duration of the demo fast), the game play appears to be simple, repetitive, and if it’s 16 hours long, probably outstays its welcome.

I picked up Wet recently, a game that sells itself on roughly the same premise of "hot girl on a vendetta" for £20 and found it repetitive, gratuitous but a fun 10 hour game.  It wasn’t a good game, but it was cheap and short enough to execute what it was attempting effectively.  It got panned.  Bayonetta was made by an influential game designer but suffers all of the same flaws of the former game, whilst suffering an uncomfortable character premise.  It’s been widely acclaimed.  I just don’t understand why.  I’d honestly pay no more than about £5 for this game, and I’d probably give up playing very quickly.

These impressions are obviously all based on the pre-release hype, marketing material and the (terribly named) "First Climax" demo.

A friend of mine quipped "if I was playing Bayonetta and somebody walked in, I’d turn it off and pretend it was porn".  I can’t help but agree with that sentiment.