Friday, February 04, 2011

Extend MVC view engine

In time of MVC1, I created a view engine to support customized view for shopping cart system. Recently I need this feature again in another project, that’s when I tried to make it a more generic way, and come across this post -

ASP.NET MVC 2 - Building extensible view engine.

It turn out to be working very well with my case with minor changes.

In my project, I want to be able to display a customized view for each carrier/program/language. The view engine will try to search the view in the following order:

// 0-View Name; 1-Controller; 2-Area, 3-Carrier; 4-Language

// 0-View Name; 1-Controller; 2-Area, 3-Carrier; 4-Language
"~/Content/Views/{3}/{1}/{0}.{4}.aspx",
"~/Content/Views/{3}/{1}/{0}.aspx",
"~/Content/Views/{3}/Shared/{0}.{4}.aspx",
"~/Content/Views/{3}/Shared/{0}.aspx",
"~/Views/{1}/{0}.{4}.aspx",
"~/Views/{1}/{0}.aspx",
"~/Views/Shared/{0}.{4}.aspx",
"~/Views/Shared/{0}.aspx",


Below is the extend view engine:

public class WebFormThemeViewEngine : WebFormExtensibleViewEngine
{

public WebFormThemeViewEngine()
{
// initialize placeholders dictionary
this.Config = new PlaceholdersDictionary();
this.Config.Add(3, GetCarrierName );
this.Config.Add(4, GetLanguageName);

// calls ValidateAndPrepareConfig method of the base class
ValidateAndPrepareConfig();

// 0-View Name; 1-Controller; 2-Area, 3-Carrier; 4-Language
// initialize *LocationFormats with appropriate values.
this.ViewLocationFormats = new string[] {
"~/Content/Views/{3}/{1}/{0}.{4}.aspx",
"~/Content/Views/{3}/{1}/{0}.aspx",
"~/Content/Views/{3}/Shared/{0}.{4}.aspx",
"~/Content/Views/{3}/Shared/{0}.aspx",
"~/Views/{1}/{0}.{4}.aspx",
"~/Views/{1}/{0}.aspx",
"~/Views/Shared/{0}.{4}.aspx",
"~/Views/Shared/{0}.aspx",
};
this.AreaViewLocationFormats = new string[] {
"~/Content/Areas/{2}/Views/{3}/{1}/{0}.{4}.aspx",
"~/Content/Areas/{2}/Views/{3}/{1}/{0}.aspx",
"~/Content/Areas/{2}/Views/{3}/Shared/{0}.{4}.aspx",
"~/Content/Areas/{2}/Views/{3}/Shared/{0}.aspx",
"~/Areas/{2}/Views/{1}/{0}.{4}.aspx",
"~/Areas/{2}/Views/{1}/{0}.aspx",
"~/Areas/{2}/Views/Shared/{0}.{4}.aspx",
"~/Areas/{2}/Views/Shared/{0}.aspx",
};
this.MasterLocationFormats = new string[] {
"~/Content/Views/{3}/Shared/{0}.{4}.master",
"~/Content/Views/{3}/Shared/{0}.master",
"~/Views/Shared/{0}.{4}.master",
"~/Views/Shared/{0}.master",
};
this.AreaMasterLocationFormats = new string[] {
"~/Content/Areas/{2}/Views/{3}/Shared/{0}.{4}.master",
"~/Content/Areas/{2}/Views/{3}/Shared/{0}.master",
"~/Areas/{2}/Views/Shared/{0}.{4}.master",
"~/Areas/{2}/Views/Shared/{0}.master",
};
this.PartialViewLocationFormats = new string[] {
"~/Content/Views/{3}/{1}/{0}.{4}.ascx",
"~/Content/Views/{3}/{1}/{0}.ascx",
"~/Content/Views/{3}/Shared/{0}.{4}.ascx",
"~/Content/Views/{3}/Shared/{0}.ascx",
"~/Views/{1}/{0}.{4}.ascx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.{4}.ascx",
"~/Views/Shared/{0}.ascx",
};
this.AreaPartialViewLocationFormats = new string[] {
"~/Content/Areas/{2}/Views/{3}/{1}/{0}.{4}.ascx",
"~/Content/Areas/{2}/Views/{3}/{1}/{0}.ascx",
"~/Content/Areas/{2}/Views/{3}/Shared/{0}.{4}.ascx",
"~/Content/Areas/{2}/Views/{3}/Shared/{0}.ascx",
"~/Areas/{2}/Views/{1}/{0}.{4}.ascx",
"~/Areas/{2}/Views/{1}/{0}.ascx",
"~/Areas/{2}/Views/Shared/{0}.{4}.ascx",
"~/Areas/{2}/Views/Shared/{0}.ascx",
};


}


protected virtual object GetCarrierName(ControllerContext controllerContext, string locationFormat, ref bool skipLocation)
{
string carrierName = controllerContext.RouteData.Values["Carrier"] as string;

return carrierName;
}

protected virtual object GetLanguageName(ControllerContext controllerContext, string locationFormat, ref bool skipLocation)
{
string language = System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
return language;
}

}


Also include the WebFormExtensibleViewEngine class from Hennadiy Kurabko.





///
/// a PlaceholderValueFunc delegate that represents a signature of method used to calculate value of the placeholder.
///
///

/// a controller context
/// location format string
/// used to specify if we must skip processed location and does not perform any searching in it.
/// an object and sends a boolean skipLocation argument by reference.
public delegate object PlaceholderValueFunc(ControllerContext controllerContext, string locationFormat, ref bool skipLocation);

///
/// link a placeholder number with calculation.
/// Integer key represents a placeholder number,
/// PlaceholderValueFunc delegate represents calculation logic.
///

public class PlaceholdersDictionary : Dictionary
{
}

///
///
///

public class WebFormExtensibleViewEngine : WebFormViewEngine
{

public WebFormExtensibleViewEngine()
: this(new PlaceholdersDictionary())
{
}

///
/// Constructor takes an instance of the PlaceholdersDictionary class as an argument
///

///
public WebFormExtensibleViewEngine(PlaceholdersDictionary config)
: base()
{
Config = config;

// calls to ValidateAndPrepareConfig method.
// It checks if {0}, {1}, {2} placeholders are used in dictionary, that is denied.
// And adds this three placeholders with anonymous delegates that simply return "{0}" for 0 placeholder, {1} for 1 placeholder
ValidateAndPrepareConfig();
}

public new string[] AreaMasterLocationFormats { get; set; }
public new string[] AreaPartialViewLocationFormats { get; set; }
public new string[] AreaViewLocationFormats { get; set; }
public new string[] ViewLocationFormats { get; set; }
public new string[] MasterLocationFormats { get; set; }
public new string[] PartialViewLocationFormats { get; set; }

protected PlaceholdersDictionary Config { get; set; }

public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
base.AreaPartialViewLocationFormats = PrepareLocationFormats(controllerContext, this.AreaPartialViewLocationFormats);
base.PartialViewLocationFormats = PrepareLocationFormats(controllerContext, this.PartialViewLocationFormats);

return base.FindPartialView(controllerContext, partialViewName, useCache);
}

public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
base.AreaViewLocationFormats = PrepareLocationFormats(controllerContext, this.AreaViewLocationFormats);
base.AreaMasterLocationFormats = PrepareLocationFormats(controllerContext, this.AreaMasterLocationFormats);
base.ViewLocationFormats = PrepareLocationFormats(controllerContext, this.ViewLocationFormats);
base.MasterLocationFormats = PrepareLocationFormats(controllerContext, this.MasterLocationFormats);

//if (string.IsNullOrEmpty(masterName))
// masterName = "Site";

return base.FindView(controllerContext, viewName, masterName, useCache);
}

///
/// checks if {0}, {1}, {2} placeholders are used in dictionary, that is denied.
///

protected virtual void ValidateAndPrepareConfig()
{
// Validate
if (Config.ContainsKey(0) || Config.ContainsKey(1) || Config.ContainsKey(2))
throw new InvalidOperationException("Placeholder index must be greater than 2. Because {0} - view name, {1} - controller name, {2} - area name.");

// Prepare
Config[0] = (ControllerContext controllerContext, string location, ref bool skipLocation) => "{0}";
Config[1] = (ControllerContext controllerContext, string location, ref bool skipLocation) => "{1}";
Config[2] = (ControllerContext controllerContext, string location, ref bool skipLocation) => "{2}";
}

protected virtual string[] PrepareLocationFormats(ControllerContext controllerContext, string[] locationFormats)
{
// First it checks if locationFormats array is null or contains no items
// simply returns this array as result if so.
if (locationFormats == null || locationFormats.Length == 0)
return locationFormats;

// it initializes locationFormatsPrepared local variable -
// a list that will be used to store locations that was prepared
// ready to be used by standard MVC mechanism.
List locationFormatsPrepared = new List();

// for every location format it creates an array of values that will be used to replace placeholders.
// Each value calculated by invoking appropriate delegate. If delegate sets skipLocation flag to true,
// processing of location will be stopped and such location will be skipped.
foreach (string locationFormat in locationFormats)
{
object[] formatValues = new object[Config.Count];

bool skipLocation = false;
for (int i = 0; i < Config.Count; i++)
{
object formatValue = Config[i](controllerContext, locationFormat, ref skipLocation);

if (skipLocation) break;

formatValues[i] = formatValue;
}
if (skipLocation) continue;

locationFormatsPrepared.Add(string.Format(locationFormat, formatValues));
}

return locationFormatsPrepared.ToArray();
}
}

1 comment:

lyon tam said...
This comment has been removed by the author.