I have an asp.net-mvc website I am working on. The site is meant to as a basis for multiple clients all with their own unique business requirements. For any given controller method, I may or may not have a customized view for the client based on their ClientId.
Right now how I am handling this is through a ResourceSelectorObject like so:
public class ClientResourceSelector
{
public ClientResourceSelector(int clientId)
{
this.ClientId = clientId;
}
public int ClientId { get; set; }
public readonly List<ViewProfile> ViewProfiles = new List<ViewProfile>()
{
new ViewProfile { ClientId = 8, Controller = "Contact", Action = "NewContact", View = "C008/NewContact" }
};
public string ViewName(string controller, string action)
{
var Profile = ViewProfiles.FirstOrDefault(X => X.Controller.Equals(controller) && X.Action.Equals(action) && X.ClientId == ClientId);
if (Profile == null) return string.Empty;
return Profile.View;
}
}
Then in the code, I use that object in this manner:
// GET: Contact/NewContact
public ActionResult NewContact()
{
var selector = new ClientResourceSelector(ClientId);
string alternate_view = selector.ViewName("Contact", "NewContact");
if (String.IsNullOrEmpty(alternate_view))
return View(NewContactViewModel.Instance(ClientId));
else
return View(alternate_view, NewContactViewModel.Instance(ClientId));
}
The problem, and this is definitely the programming equivalent of "First World Problems," but I would like to still be able to just call View(viewModel) and have it select the appropriate view to display programmatically without my having to remember to register each view in the selector.
Obviously, I would then want to override the View() method in the abstract controller that all of my controllers are inheriting from. But I am unsure of how that code would look. Any suggestions would be helpful.
Here is how I've created ones in the past. Most of the Tenant systems I've built use some type of route/request parameter (could easily be updated to use DNS or wahtever, you have a lot of options) to determine the specific Tenant. I use an action filter that executes before any controller (or routing) to populate the route data (useful for Tenant specific routes as well).
public class TenantActionFilterAttribute : ActionFilterAttribute
{
internal const string _Tenant = "tenant";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Do this how ever you want, right now I'm using querystring
// Could be changed to use DNS name or whatever
var tenant = filterContext.HttpContext.Request.QueryString[_Tenant] as string;
if (tenant != null)
{
filterContext.RouteData.Values[Tenant] = tenant;
}
}
}
Either globally register the action filter:
RegisterGlobalFilters(GlobalFilters.Filters);
(Or using a Dependency Injection Framework)
Then a custom ViewEngine:
public class TenantViewEngine : RazorViewEngine
{
private string GetPrefix(ControllerContext controllerContext)
{
var result = string.Empty;
var tenant = controllerContext.RouteData.Values[TenantActionFilterAttribute.Tenant] as string;
if (!string.IsNullOrEmpty(tenant))
{
result = "Tenants/" + tenant + "/";
}
return result;
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var prefix = GetPrefix(controllerContext);
if (partialPath.StartsWith("~/"))
{
partialPath = partialPath.Insert(2, prefix);
}
else if (partialPath.StartsWith("~") || partialPath.StartsWith("/"))
{
partialPath = partialPath.Insert(1, prefix);
}
else if (string.IsNullOrEmpty(partialPath))
{
partialPath = prefix + partialPath;
}
return base.CreatePartialView(controllerContext, partialPath);
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var prefix = GetPrefix(controllerContext);
if (viewPath.StartsWith("~/"))
{
viewPath = viewPath.Insert(2, prefix);
}
else if (viewPath.StartsWith("~") || viewPath.StartsWith("/"))
{
viewPath = viewPath.Insert(1, prefix);
}
else if (!string.IsNullOrEmpty(viewPath))
{
viewPath = prefix + viewPath;
}
if (masterPath.StartsWith("~/"))
{
masterPath = masterPath.Insert(2, prefix);
}
else if (masterPath.StartsWith("~") || masterPath.StartsWith("/"))
{
masterPath = masterPath.Insert(1, prefix);
}
else if (!string.IsNullOrEmpty(masterPath))
{
masterPath = prefix + masterPath;
}
return base.CreateView(controllerContext, viewPath, masterPath);
}
}
I can't exactly remember how this works, but the search paths change from the default to something very close to:
"~/Tenants/<TenantName>/Areas/{3}/Views/{1}/{0}.cshtml",
"~/Areas/{3}/Views/{1}/{0}.cshtml",
"~/Tenants/<TenantName>//Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Tenants/<TenantName>//Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.cshtml",
Where 1:Controller, 2:View/Action, 3:AreaName
Related
I have query string class.
public class PagingModel
{
public int PageNumber { get; set; } = 1;
public string Filter { get; set; } = "text";
}
string url = "Menu/GetMenus";
I have to generate the URI with a query string based on an object in ASP.NET Core 5 preview. Is there any built in query helper?.
Required output:
/Menu/GetMenus?PageNumber=3&Filter=text
MVC Controller:
public async Task<IActionResult> index_partial([FromQuery] PagingModel paging)
{
var data = await _apiService.GetMenusAsync(paging);
return PartialView("_IndexPartial", data);
}
Service:
public async Task<PagedList<MenuModel>> GetMenusAsync(PagingModel paging)
{
string Requiredurl = "Menu/GetMenus?page="+ paging.PageNumber;
}
I got this extension method.. No need to generate query string manually.Only class object we need to pass. i thought some one else can use the same thing ...
public static string AppendObjectToQueryString(string uri, object requestObject)
{
var type = requestObject.GetType();
var data = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary
(
p => p.Name,
p => p.GetValue(requestObject)
);
foreach (var d in data)
{
if (d.Value == null)
{
continue;
}
if ((d.Value as string == null) && d.Value is IEnumerable enumerable)
{
foreach (var value in enumerable)
{
uri = QueryHelpers.AddQueryString(uri, d.Key, value.ToString());
}
}
else
{
uri = QueryHelpers.AddQueryString(uri, d.Key, d.Value.ToString());
}
}
return uri;
}
Ex: In my case i called this way.
string uri = "Menu/GetMenus";
string full_uri = QueryStringExtension.AppendObjectToQueryString(uri, paging);
With a Query String this simple I would just do
PagingModel qsData = new PagingModel();
//set qsData properties as needed
string urlWithQueryString = $"/Menu/GetMenus?{nameof(PagingModel.PageNumber)}={qsData.PageNumber}&nameof(PagingModel.Filter)}={qsData.Filter}";
However more standard is to do something like
string urlWithQueryString = this.Url.Action("GetMenus", "Menu", new PagingModel { PageNumber = 3, Filter = "text" }, this.Request.Url.Scheme);
But best solution depends on your specific case - can you add your action method definition for GetMenus ?
Update for your additional code :
Seeing as looks like you want to generate the url inside the service I would simply do this :
public async Task<PagedList<MenuModel>> GetMenusAsync(PagingModel paging)
{
string Requiredurl = $"/Menu/GetMenus?{nameof(PagingModel.PageNumber)}={paging.PageNumber}&nameof(PagingModel.Filter)}={paging.Filter}";
}
using asp.net mvc 5
I have create a Custom conditional validation which is working perfectly when it is getting posted to controller but not working in client side -
and I am showing the code like below
if (!ModelState.IsValid)
{
return View("Index", model);
}
1 .I could not find any way in asp.net mvc which will work without the Jquery code - if you can suggest or show any other way without writing client side code manually that is preferable
If its not possible without writing client side code by myself , then how can i send two three error message from server , make it work in the client side
View Model
[ValidateTargetId]
public string TargetId{ get; set; }
[Required]
public string DependentProperty{ get; set; }
public SelectList DependentPropertyDropDownList { get; set; }
Custom Validator class
public class ValidateTargetIdAttribute : ValidationAttribute , IClientValidatable
{
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
object instance = validationContext.ObjectInstance;
Type type = instance.GetType();
PropertyInfo property = type.GetProperty("DependentProperty");
object propertyValue = property.GetValue(instance);
value = value == null ? "" : value.ToString();
if (propertyValue == null)
{
return null;
}
else
{
switch (propertyValue.ToString().ToUpper())
{
case "Case1":
if (!string.IsNullOrEmpty(value.ToString()))
{
return new
ValidationResult("target id should be blank");
}
break;
case "Case2":
if (value.ToString().Trim().Length != 15)
{
return new ValidationResult("Target Id should have length 15 ");
}
break;
case "Case3":
if (value.ToString().Trim().Length != 20)
{
return new ValidationResult("Target Id Should have length 20");
}
break;
}
return null;
}
}
//for client side
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
ModelClientValidationRule mvr = new ModelClientValidationRule();
mvr.ErrorMessage = "target id should be blank ";
mvr.ValidationType = "validtarget";
return new[] { mvr };
}
}
Jquery
$(function() {
jQuery.validator.addMethod('validtarget', function (value, element, params) {
// how to create this logic
}, '');
jQuery.validator.unobtrusive.adapters.add('validtarget', function (options) {
options.rules['validtarget] = {};
options.messages['validtarget'] = options.message;
});
}(jQuery));
I have the following layout for my mvc project:
/Controllers
/Demo
/Demo/DemoArea1Controller
/Demo/DemoArea2Controller
etc...
/Views
/Demo
/Demo/DemoArea1/Index.aspx
/Demo/DemoArea2/Index.aspx
However, when I have this for DemoArea1Controller:
public class DemoArea1Controller : Controller
{
public ActionResult Index()
{
return View();
}
}
I get the "The view 'index' or its master could not be found" error, with the usual search locations.
How can I specify that controllers in the "Demo" namespace search in the "Demo" view subfolder?
You can easily extend the WebFormViewEngine to specify all the locations you want to look in:
public class CustomViewEngine : WebFormViewEngine
{
public CustomViewEngine()
{
var viewLocations = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx",
"~/AnotherPath/Views/{0}.ascx"
// etc
};
this.PartialViewLocationFormats = viewLocations;
this.ViewLocationFormats = viewLocations;
}
}
Make sure you remember to register the view engine by modifying the Application_Start method in your Global.asax.cs
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());
}
Now in MVC 6 you can implement IViewLocationExpander interface without messing around with view engines:
public class MyViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context) {}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
return new[]
{
"/AnotherPath/Views/{1}/{0}.cshtml",
"/AnotherPath/Views/Shared/{0}.cshtml"
}; // add `.Union(viewLocations)` to add default locations
}
}
where {0} is target view name, {1} - controller name and {2} - area name.
You can return your own list of locations, merge it with default viewLocations (.Union(viewLocations)) or just change them (viewLocations.Select(path => "/AnotherPath" + path)).
To register your custom view location expander in MVC, add next lines to ConfigureServices method in Startup.cs file:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new MyViewLocationExpander());
});
}
There's actually a lot easier method than hardcoding the paths into your constructor. Below is an example of extending the Razor engine to add new paths. One thing I'm not entirely sure about is whether the paths you add here will be cached:
public class ExtendedRazorViewEngine : RazorViewEngine
{
public void AddViewLocationFormat(string paths)
{
List<string> existingPaths = new List<string>(ViewLocationFormats);
existingPaths.Add(paths);
ViewLocationFormats = existingPaths.ToArray();
}
public void AddPartialViewLocationFormat(string paths)
{
List<string> existingPaths = new List<string>(PartialViewLocationFormats);
existingPaths.Add(paths);
PartialViewLocationFormats = existingPaths.ToArray();
}
}
And your Global.asax.cs
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ExtendedRazorViewEngine engine = new ExtendedRazorViewEngine();
engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.cshtml");
engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.vbhtml");
// Add a shared location too, as the lines above are controller specific
engine.AddPartialViewLocationFormat("~/MyThemes/{0}.cshtml");
engine.AddPartialViewLocationFormat("~/MyThemes/{0}.vbhtml");
ViewEngines.Engines.Add(engine);
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
One thing to note: your custom location will need the ViewStart.cshtml file in its root.
If you want just add new paths, you can add to the default view engines and spare some lines of code:
ViewEngines.Engines.Clear();
var razorEngine = new RazorViewEngine();
razorEngine.MasterLocationFormats = razorEngine.MasterLocationFormats
.Concat(new[] {
"~/custom/path/{0}.cshtml"
}).ToArray();
razorEngine.PartialViewLocationFormats = razorEngine.PartialViewLocationFormats
.Concat(new[] {
"~/custom/path/{1}/{0}.cshtml", // {1} = controller name
"~/custom/path/Shared/{0}.cshtml"
}).ToArray();
ViewEngines.Engines.Add(razorEngine);
The same applies to WebFormEngine
Instead of subclassing the RazorViewEngine, or replacing it outright, you can just alter existing RazorViewEngine's PartialViewLocationFormats property. This code goes in Application_Start:
System.Web.Mvc.RazorViewEngine rve = (RazorViewEngine)ViewEngines.Engines
.Where(e=>e.GetType()==typeof(RazorViewEngine))
.FirstOrDefault();
string[] additionalPartialViewLocations = new[] {
"~/Views/[YourCustomPathHere]"
};
if(rve!=null)
{
rve.PartialViewLocationFormats = rve.PartialViewLocationFormats
.Union( additionalPartialViewLocations )
.ToArray();
}
Last I checked, this requires you to build your own ViewEngine. I don't know if they made it easier in RC1 though.
The basic approach I used before the first RC was, in my own ViewEngine, to split the namespace of the controller and look for folders which matched the parts.
EDIT:
Went back and found the code. Here's the general idea.
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
string ns = controllerContext.Controller.GetType().Namespace;
string controller = controllerContext.Controller.GetType().Name.Replace("Controller", "");
//try to find the view
string rel = "~/Views/" +
(
ns == baseControllerNamespace ? "" :
ns.Substring(baseControllerNamespace.Length + 1).Replace(".", "/") + "/"
)
+ controller;
string[] pathsToSearch = new string[]{
rel+"/"+viewName+".aspx",
rel+"/"+viewName+".ascx"
};
string viewPath = null;
foreach (var path in pathsToSearch)
{
if (this.VirtualPathProvider.FileExists(path))
{
viewPath = path;
break;
}
}
if (viewPath != null)
{
string masterPath = null;
//try find the master
if (!string.IsNullOrEmpty(masterName))
{
string[] masterPathsToSearch = new string[]{
rel+"/"+masterName+".master",
"~/Views/"+ controller +"/"+ masterName+".master",
"~/Views/Shared/"+ masterName+".master"
};
foreach (var path in masterPathsToSearch)
{
if (this.VirtualPathProvider.FileExists(path))
{
masterPath = path;
break;
}
}
}
if (string.IsNullOrEmpty(masterName) || masterPath != null)
{
return new ViewEngineResult(
this.CreateView(controllerContext, viewPath, masterPath), this);
}
}
//try default implementation
var result = base.FindView(controllerContext, viewName, masterName);
if (result.View == null)
{
//add the location searched
return new ViewEngineResult(pathsToSearch);
}
return result;
}
Try something like this:
private static void RegisterViewEngines(ICollection<IViewEngine> engines)
{
engines.Add(new WebFormViewEngine
{
MasterLocationFormats = new[] {"~/App/Views/Admin/{0}.master"},
PartialViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.ascx"},
ViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.aspx"}
});
}
protected void Application_Start()
{
RegisterViewEngines(ViewEngines.Engines);
}
Note: for ASP.NET MVC 2 they have additional location paths you will need to set for views in 'Areas'.
AreaViewLocationFormats
AreaPartialViewLocationFormats
AreaMasterLocationFormats
Creating a view engine for an Area is described on Phil's blog.
Note: This is for preview release 1 so is subject to change.
Most of the answers here, clear the existing locations by calling ViewEngines.Engines.Clear() and then add them back in again... there is no need to do this.
We can simply add the new locations to the existing ones, as shown below:
// note that the base class is RazorViewEngine, NOT WebFormViewEngine
public class ExpandedViewEngine : RazorViewEngine
{
public ExpandedViewEngine()
{
var customViewSubfolders = new[]
{
// {1} is conroller name, {0} is action name
"~/Areas/AreaName/Views/Subfolder1/{1}/{0}.cshtml",
"~/Areas/AreaName/Views/Subfolder1/Shared/{0}.cshtml"
};
var customPartialViewSubfolders = new[]
{
"~/Areas/MyAreaName/Views/Subfolder1/{1}/Partials/{0}.cshtml",
"~/Areas/MyAreaName/Views/Subfolder1/Shared/Partials/{0}.cshtml"
};
ViewLocationFormats = ViewLocationFormats.Union(customViewSubfolders).ToArray();
PartialViewLocationFormats = PartialViewLocationFormats.Union(customPartialViewSubfolders).ToArray();
// use the following if you want to extend the master locations
// MasterLocationFormats = MasterLocationFormats.Union(new[] { "new master location" }).ToArray();
}
}
Now you can configure your project to use the above RazorViewEngine in Global.asax:
protected void Application_Start()
{
ViewEngines.Engines.Add(new ExpandedViewEngine());
// more configurations
}
See this tutoral for more info.
I did it this way in MVC 5. I didn't want to clear the default locations.
Helper Class:
namespace ConKit.Helpers
{
public static class AppStartHelper
{
public static void AddConKitViewLocations()
{
// get engine
RazorViewEngine engine = ViewEngines.Engines.OfType<RazorViewEngine>().FirstOrDefault();
if (engine == null)
{
return;
}
// extend view locations
engine.ViewLocationFormats =
engine.ViewLocationFormats.Concat(new string[] {
"~/Views/ConKit/{1}/{0}.cshtml",
"~/Views/ConKit/{0}.cshtml"
}).ToArray();
// extend partial view locations
engine.PartialViewLocationFormats =
engine.PartialViewLocationFormats.Concat(new string[] {
"~/Views/ConKit/{0}.cshtml"
}).ToArray();
}
}
}
And then in Application_Start:
// Add ConKit View locations
ConKit.Helpers.AppStartHelper.AddConKitViewLocations();
How can I use Same Custom Validation Attribute Multiple Times on Same Field or simply enable AllowMultiple=true, for both server side and client side validation??
I have a following Custom Validation Attribute:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,
AllowMultiple = true, Inherited = true)]
public class RequiredIfAttribute : ValidationAttribute,IClientValidatable
{
public RequiredIfAttribute(string dependentProperties,
string dependentValues = "",
string requiredValue = "val")
{
}
}
Where in dependentProperties I can specify multiple dependant properties seperated by comma, in dependentValues I can specify for which values of dependant properties validation should process and finally in requiredValue I can specify expected value for the field to be validated.
In my model there are two properties LandMark, PinCode and I want to use validation as follows:
public string LandMark { get; set; }
[RequiredIf("LandMark","XYZ","500500")]
[RequiredIf("LandMark", "ABC", "500505")]
public string PinCode { get; set; }
The values here are just for example, as per it seems I can add the attribute multiple times and don't get any compile error, I have implemented TypeID in attribute and it works well from serverside if I remove client validation from it. But when I am implementing IClientValidatable on the attribute, it gives me an error:
"Validation type names in unobtrusive client validation rules must be unique."
Any help how can I solve it??
The Problem
Validation Attributes have two environments they can validate against:
Server
Client
Server Validation - Multiple Attributes Easy
If you have any attribute with:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class RequiredIfAttribute : ValidationAttribute
And have put it on your class property like this:
public class Client
{
public short ResidesWithCd { get; set; };
[RequiredIf(nameof(ResidesWithCd), new[] { 99 }, "Resides with other is required.")]
public string ResidesWithOther { get; set; }
}
Then anytime the Server goes to validate an object (ex. ModelState.IsValid), it will check every ValidationAttribute on each property and call .IsValid() to determine validity. This will work fine, even if AttributeUsage.AllowMultiple is set to true.
Client Validation - HTML Attribute Bottleneck
If you enable client side by implementing IClientValidatable like this:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "requiredif",
ErrorMessage = ErrorMessageString
};
modelClientValidationRule.ValidationParameters.Add("target", prop.PropName);
modelClientValidationRule.ValidationParameters.Add("values", prop.CompValues);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
Then ASP.NET will emit the following HTML when generated:
(As long as ClientValidationEnabled &
UnobtrusiveJavaScriptEnabled are enabled)
<input class="form-control" type="text" value=""
id="Client_CommunicationModificationDescription"
name="Client.CommunicationModificationDescription"
data-val="true"
data-val-requiredif="Communication Modification Description is required."
data-val-requiredif-target="CommunicationModificationCd"
data-val-requiredif-values="99" >
Data Attributes are the only vehicle we have for dumping rules into the client side validation engine which will search for any attributes on the page via a built in or custom adapter. And once part of the set of client side rules, it'll be able to determine the validity of each parsed rule with a built in or custom method.
So we can call jQuery Validate Unobtrusive to look for and parse these attributes by adding a custom adapter which will add a validation rule to the engine:
// hook up to client side validation
$.validator.unobtrusive.adapters.add('requiredif', ['target', 'values'], function (options) {
options.rules["requiredif"] = {
id: '#' + options.params.target,
values: JSON.parse(options.params.values)
};
options.messages['requiredif'] = options.message;
});
We can then tell that rule how function and determine validity by adding a custom method like this which will add a custom way to evaluate requiredif rules (as opposed to date rules or regex rules) which will rely on the parameters we loaded earlier through the adapter:
// test validity
$.validator.addMethod('requiredif', function (value, element, params) {
var targetHasCondValue = targetElHasValue(params.id, params.value);
var requiredAndNoValue = targetHasCondValue && !value; // true -> :(
var passesValidation = !requiredAndNoValue; // true -> :)
return passesValidation;
}, '');
Which all operates like this:
Solution
So, what have we learned? Well, if we want the same rule to appear multiple times on the same element, the adapter would have to see the exact set of rules multiple times per element, with no way to differentiate between each instance within multiple sets. Further, ASP.NET won't render the same attribute name multiple times since it's not valid html.
So, we either need to:
Collapse all the client side rules into a single mega attribute with all the info
Rename attributes with each instance number and then find a way to parse them in sets.
I'll explore Option One (emitting a single client side attribute), which you could do a couple ways:
Create a single Attribute that takes in multiple elements to validate on the server client
Keep multiple distinct server side attributes and then merge all attributes via reflection before emitting to the client
In either case you will have to re-write the client side logic (adapter/method) to take an array of values, instead of a single value at a time.
To we'll build/transmit a JSON serialized object that looks like this:
var props = [
{
PropName: "RoleCd",
CompValues: ["2","3","4","5"]
},
{
PropName: "IsPatient",
CompValues: ["true"]
}
]
Scripts/ValidateRequiredIfAny.js
Here's how we'll handle that in client side adapter / method:
// hook up to client side validation
$.validator.unobtrusive.adapters.add("requiredifany", ["props"], function (options) {
options.rules["requiredifany"] = { props: options.params.props };
options.messages["requiredifany"] = options.message;
});
// test validity
$.validator.addMethod("requiredifany", function (value, element, params) {
var reqIfProps = JSON.parse(params.props);
var anytargetHasValue = false;
$.each(reqIfProps, function (index, item) {
var targetSel = "#" + buildTargetId(element, item.PropName);
var $targetEl = $(targetSel);
var targetHasValue = elHasValue($targetEl, item.CompValues);
if (targetHasValue) {
anytargetHasValue = true;
return ;
}
});
var valueRequired = anytargetHasValue;
var requiredAndNoValue = valueRequired && !value; // true -> :(
var passesValidation = !requiredAndNoValue; // true -> :)
return passesValidation;
}, "");
// UTILITY METHODS
function buildTargetId(currentElement, targetPropName) {
// https://stackoverflow.com/a/39725539/1366033
// we are only provided the name of the target property
// we need to build it's ID in the DOM based on a couple assumptions
// derive the stacking context and depth based on the current element's ID/name
// append the target property's name to that context
// currentElement.name i.e. Details[0].DosesRequested
var curId = currentElement.id; // get full id i.e. Details_0__DosesRequested
var context = curId.replace(/[^_]+$/, ""); // remove last prop i.e. Details_0__
var targetId = context + targetPropName; // build target ID i.e. Details_0__OrderIncrement
// fail noisily
if ($("#" + targetId).length === 0)
console.error(
"Could not find id '" + targetId +
"' when looking for '" + targetPropName +
"' on originating element '" + curId + "'");
return targetId;
}
function elHasValue($el, values) {
var isCheckBox = $el.is(":checkbox,:radio");
var isChecked = $el.is(":checked");
var inputValue = $el.val();
var valueInArray = $.inArray(String(inputValue), values) > -1;
var hasValue = (!isCheckBox || isChecked) && valueInArray;
return hasValue;
};
Models/RequiredIfAttribute.cs
On the server side, we'll validate attributes like normal, but when we got to build the client side attributes, we'll look for all attributes and build one mega attribute
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Web.Helpers;
using System.Web.Mvc;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{
public PropertyNameValues TargetProp { get; set; }
public RequiredIfAttribute(string compPropName, string[] compPropValues, string msg) : base(msg)
{
this.TargetProp = new PropertyNameValues()
{
PropName = compPropName,
CompValues = compPropValues
};
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo compareProp = validationContext.ObjectType.GetProperty(TargetProp.PropName);
var compPropVal = compareProp.GetValue(validationContext.ObjectInstance, null);
string compPropValAsString = compPropVal?.ToString().ToLower() ?? "";
var matches = TargetProp.CompValues.Where(v => v == compPropValAsString);
bool needsValue = matches.Any();
if (needsValue)
{
if (value == null || value.ToString() == "" || value.ToString() == "0")
{
return new ValidationResult(FormatErrorMessage(null));
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
// at this point, who cares that we're on this particular instance - find all instances
PropertyInfo curProp = metadata.ContainerType.GetProperty(metadata.PropertyName);
RequiredIfAttribute[] allReqIfAttr = curProp.GetCustomAttributes<RequiredIfAttribute>().ToArray();
// emit validation attributes from all simultaneously, otherwise each will overwrite the last
PropertyNameValues[] allReqIfInfo = allReqIfAttr.Select(x => x.TargetProp).ToArray();
string allReqJson = Json.Encode(allReqIfInfo);
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "requiredifany",
ErrorMessage = ErrorMessageString
};
// add name for jQuery parameters for the adapter, must be LOWERCASE!
modelClientValidationRule.ValidationParameters.Add("props", allReqJson);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
}
public class PropertyNameValues
{
public string PropName { get; set; }
public string[] CompValues { get; set; }
}
Then we can bind that to our model by applying multiple attributes simultaneously:
[RequiredIf(nameof(RelationshipCd), new[] { 1,2,3,4,5 }, "Mailing Address is required.")]
[RequiredIf(nameof(IsPatient), new[] { "true" },"Mailing Address is required.")]
public string MailingAddressLine1 { get; set; }
Further Reading
ASP.NET MVC custom multiple fields validation by Stephen Muecke
Unobtrusive Client Validation in ASP.NET MVC 3 by Brad Wilson
Finally here I found the answer my-self.
Look at following article for solution
http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx
The link in the accepted answer (http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx) is buggy, and someone else has written an errata here which I would recommend reading first. The answer above does not handle inheritance.
I believe this alternate solution has some advantages (including support of inheritance), but remains far from perfect code - improvements appreciated.
this C# uses Json.NET and Stuart Leeks HTML Attribute provider
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using Newtonsoft.Json;
namespace DabTrial.Infrastructure.Validation
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public abstract class MultipleValidationAttribute : ValidationAttribute, IMetadataAware
{
private class Validation
{
public ICollection<string> ErrorMessage { get; set; }
public IDictionary<string, ICollection<object>> Attributes { get; set; }
}
private object _typeId = new object();
public const string attributeName = "multipleValidations";
public MultipleValidationAttribute()
{
}
public override object TypeId
{
get
{
return this._typeId;
}
}
public void OnMetadataCreated(ModelMetadata metadata)
{
Dictionary<string, Validation> allMultis;
if (metadata.AdditionalValues.ContainsKey(attributeName))
{
allMultis = (Dictionary<string, Validation>)metadata.AdditionalValues[attributeName];
}
else
{
allMultis = new Dictionary<string, Validation>();
metadata.AdditionalValues.Add(attributeName, allMultis);
}
foreach (var result in GetClientValidationRules(metadata))
{
if (allMultis.ContainsKey(result.ValidationType))
{
var thisMulti = allMultis[result.ValidationType];
thisMulti.ErrorMessage.Add(result.ErrorMessage);
foreach (var attr in result.ValidationParameters)
{
thisMulti.Attributes[attr.Key].Add(attr.Value);
}
}
else
{
var thisMulti = new Validation
{
ErrorMessage = new List<string>(),
Attributes = new Dictionary<string, ICollection<object>>()
};
allMultis.Add(result.ValidationType, thisMulti);
thisMulti.ErrorMessage.Add(result.ErrorMessage);
foreach (var attr in result.ValidationParameters)
{
var newList = new List<object>();
newList.Add(attr.Value);
thisMulti.Attributes.Add(attr.Key, newList);
}
}
}
}
public static IEnumerable<KeyValuePair<string, object>> GetAttributes(ModelMetadata metadata)
{
if (!metadata.AdditionalValues.ContainsKey(attributeName))
{
return null;
}
var returnVar = new List<KeyValuePair<string, object>>();
returnVar.Add(new KeyValuePair<string,object>("data-val", true));
var allMultis = (Dictionary<string, Validation>)metadata.AdditionalValues[attributeName];
foreach (var multi in allMultis)
{
string valName = "data-val-" + multi.Key;
returnVar.Add(new KeyValuePair<string,object>(valName, JsonConvert.SerializeObject(multi.Value.ErrorMessage)));
returnVar.AddRange(multi.Value.Attributes.Select(a=>new KeyValuePair<string,object>(valName + '-' + a.Key, JsonConvert.SerializeObject(a.Value))));
}
return returnVar;
}
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
ControllerContext context)
{
throw new NotImplementedException("This function must be overriden");
}
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata)
{
return GetClientValidationRules(metadata, null);
}
}
}
the Global.asax contains the code
HtmlAttributeProvider.Register((metadata) =>
{
return MultipleValidationAttribute.GetAttributes(metadata);
});
and the JavaScript (within a custom validators function)
function setMultiValidationValues(options, ruleName, values) {
var i = 0, thisRule;
for (; i < values.length; i++) {
thisRule = (i == 0) ? ruleName : ruleName + i;
options.messages[thisRule] = values[i].message;
delete values[i].message;
options.rules[thisRule] = values[i];
if (ruleName !== thisRule) {
(function addValidatorMethod() {
var counter = 0;
if (!$.validator.methods[ruleName]) {
if (++counter > 10) { throw new ReferenceError(ruleName + " is not defined"); }
setTimeout(addValidatorMethod, 100);
return;
}
if (!$.validator.methods[thisRule]) { $.validator.addMethod(thisRule, $.validator.methods[ruleName]); }
})();
}
}
}
function transformValidationValues(options) {
var rules = $.parseJSON(options.message),
propNames = [], p, utilObj,i = 0,j, returnVar=[];
for (p in options.params) {
if (options.params.hasOwnProperty(p)) {
utilObj = {};
utilObj.key = p;
utilObj.vals = $.parseJSON(options.params[p]);
propNames.push(utilObj);
}
}
for (; i < rules.length; i++) {
utilObj = {};
utilObj.message = rules[i];
for (j=0; j < propNames.length; j++) {
utilObj[propNames[j].key] = propNames[j].vals[i];
}
returnVar.push(utilObj);
}
return returnVar;
}
An example of its use is below:
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using System.Web.Mvc;
namespace DabTrial.Infrastructure.Validation
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class RegexCountAttribute : MultipleValidationAttribute
{
# region members
private string _defaultErrorMessageFormatString;
protected readonly string _regexStr;
protected readonly RegexOptions _regexOpt;
private int _minimumCount=0;
private int _maximumCount=int.MaxValue;
#endregion
#region properties
public int MinimumCount
{
get { return _minimumCount; }
set
{
if (value < 0) { throw new ArgumentOutOfRangeException(); }
_minimumCount = value;
}
}
public int MaximumCount
{
get { return _maximumCount; }
set
{
if (value < 0) { throw new ArgumentOutOfRangeException(); }
_maximumCount = value;
}
}
private string DefaultErrorMessageFormatString
{
get
{
if (_defaultErrorMessageFormatString == null)
{
_defaultErrorMessageFormatString = string.Format(
"{{0}} requires a {0}{1}{2} match(es) to regex {3}",
MinimumCount>0?"minimum of "+ MinimumCount:"",
MinimumCount > 0 && MaximumCount< int.MaxValue? " and " : "",
MaximumCount<int.MaxValue?"maximum of "+ MaximumCount:"",
_regexStr);
}
return _defaultErrorMessageFormatString;
}
set
{
_defaultErrorMessageFormatString = value;
}
}
#endregion
#region instantiation
public RegexCountAttribute(string regEx, string defaultErrorMessageFormatString = null, RegexOptions regexOpt = RegexOptions.None)
{
#if debug
if (minimumCount < 0) { throw new ArgumentException("the minimum value must be non-negative"); }
#endif
_regexStr = regEx;
DefaultErrorMessageFormatString = defaultErrorMessageFormatString;
_regexOpt = regexOpt;
}
#endregion
#region methods
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var instr = (string)value;
int matchCount = 0;
if (MinimumCount > 0 && instr != null)
{
Match match = new Regex(_regexStr,_regexOpt).Match(instr);
while (match.Success && ++matchCount < MinimumCount)
{
match = match.NextMatch();
}
if (MaximumCount != int.MaxValue)
{
while (match.Success && ++matchCount <= MaximumCount)
{
match = match.NextMatch();
}
}
}
if (matchCount >= MinimumCount && matchCount <=MaximumCount)
{
return ValidationResult.Success;
}
string errorMessage = GetErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
protected string GetErrorMessage(string displayName)
{
return ErrorMessage ?? string.Format(DefaultErrorMessageFormatString,
displayName,
MinimumCount);
}
private bool HasFlag(RegexOptions options, RegexOptions flag)
{
return ((options & flag) == flag);
}
private string RegexpModifier
{
get
{
string options = string.Empty;
if (HasFlag(_regexOpt, RegexOptions.IgnoreCase)) { options += 'i'; }
if (HasFlag(_regexOpt, RegexOptions.Multiline)) { options += 'm'; }
return options;
}
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata)
{
var returnVal = new ModelClientValidationRule {
ErrorMessage = GetErrorMessage(metadata.DisplayName),
ValidationType = "regexcount",
};
returnVal.ValidationParameters.Add("min",MinimumCount);
returnVal.ValidationParameters.Add("max",MaximumCount);
returnVal.ValidationParameters.Add("regex",_regexStr);
returnVal.ValidationParameters.Add("regexopt", RegexpModifier);
yield return returnVal;
}
#endregion
}
public class MinNonAlphanum : RegexCountAttribute
{
public MinNonAlphanum(int minimum) : base("[^0-9a-zA-Z]", GetDefaultErrorMessageFormatString(minimum))
{
this.MinimumCount = minimum;
}
private static string GetDefaultErrorMessageFormatString(int min)
{
if (min == 1)
{
return "{0} requires a minimum of {1} character NOT be a letter OR number";
}
return "{0} requires a minimum of {1} characters NOT be a letter OR number";
}
}
public class MinDigits : RegexCountAttribute
{
public MinDigits(int minimum) : base(#"\d", GetDefaultErrorMessageFormatString(minimum))
{
this.MinimumCount = minimum;
}
private static string GetDefaultErrorMessageFormatString(int min)
{
if (min == 1)
{
return "{0} requires a minimum of {1} character is a number";
}
return "{0} requires a minimum of {1} characters are numbers";
}
}
}
JavaScript:
$.validator.addMethod("regexcount", function (value, element, params) {
var matches = (value.match(params.regex)||[]).length
return matches >= params.min && matches <= params.max;
});
$.validator.unobtrusive.adapters.add("regexcount", ["min", "max", "regex", "regexopt"], function (options) {
var args = transformValidationValues(options), i=0;
for (; i < args.length; i++) {
args[i].regex = new RegExp(args[i].regex, args[i].regexopt);
delete args[i].regexopt;
}
setMultiValidationValues(options, "regexcount", args);
});
I have the following layout for my mvc project:
/Controllers
/Demo
/Demo/DemoArea1Controller
/Demo/DemoArea2Controller
etc...
/Views
/Demo
/Demo/DemoArea1/Index.aspx
/Demo/DemoArea2/Index.aspx
However, when I have this for DemoArea1Controller:
public class DemoArea1Controller : Controller
{
public ActionResult Index()
{
return View();
}
}
I get the "The view 'index' or its master could not be found" error, with the usual search locations.
How can I specify that controllers in the "Demo" namespace search in the "Demo" view subfolder?
You can easily extend the WebFormViewEngine to specify all the locations you want to look in:
public class CustomViewEngine : WebFormViewEngine
{
public CustomViewEngine()
{
var viewLocations = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx",
"~/AnotherPath/Views/{0}.ascx"
// etc
};
this.PartialViewLocationFormats = viewLocations;
this.ViewLocationFormats = viewLocations;
}
}
Make sure you remember to register the view engine by modifying the Application_Start method in your Global.asax.cs
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());
}
Now in MVC 6 you can implement IViewLocationExpander interface without messing around with view engines:
public class MyViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context) {}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
return new[]
{
"/AnotherPath/Views/{1}/{0}.cshtml",
"/AnotherPath/Views/Shared/{0}.cshtml"
}; // add `.Union(viewLocations)` to add default locations
}
}
where {0} is target view name, {1} - controller name and {2} - area name.
You can return your own list of locations, merge it with default viewLocations (.Union(viewLocations)) or just change them (viewLocations.Select(path => "/AnotherPath" + path)).
To register your custom view location expander in MVC, add next lines to ConfigureServices method in Startup.cs file:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new MyViewLocationExpander());
});
}
There's actually a lot easier method than hardcoding the paths into your constructor. Below is an example of extending the Razor engine to add new paths. One thing I'm not entirely sure about is whether the paths you add here will be cached:
public class ExtendedRazorViewEngine : RazorViewEngine
{
public void AddViewLocationFormat(string paths)
{
List<string> existingPaths = new List<string>(ViewLocationFormats);
existingPaths.Add(paths);
ViewLocationFormats = existingPaths.ToArray();
}
public void AddPartialViewLocationFormat(string paths)
{
List<string> existingPaths = new List<string>(PartialViewLocationFormats);
existingPaths.Add(paths);
PartialViewLocationFormats = existingPaths.ToArray();
}
}
And your Global.asax.cs
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ExtendedRazorViewEngine engine = new ExtendedRazorViewEngine();
engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.cshtml");
engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.vbhtml");
// Add a shared location too, as the lines above are controller specific
engine.AddPartialViewLocationFormat("~/MyThemes/{0}.cshtml");
engine.AddPartialViewLocationFormat("~/MyThemes/{0}.vbhtml");
ViewEngines.Engines.Add(engine);
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
One thing to note: your custom location will need the ViewStart.cshtml file in its root.
If you want just add new paths, you can add to the default view engines and spare some lines of code:
ViewEngines.Engines.Clear();
var razorEngine = new RazorViewEngine();
razorEngine.MasterLocationFormats = razorEngine.MasterLocationFormats
.Concat(new[] {
"~/custom/path/{0}.cshtml"
}).ToArray();
razorEngine.PartialViewLocationFormats = razorEngine.PartialViewLocationFormats
.Concat(new[] {
"~/custom/path/{1}/{0}.cshtml", // {1} = controller name
"~/custom/path/Shared/{0}.cshtml"
}).ToArray();
ViewEngines.Engines.Add(razorEngine);
The same applies to WebFormEngine
Instead of subclassing the RazorViewEngine, or replacing it outright, you can just alter existing RazorViewEngine's PartialViewLocationFormats property. This code goes in Application_Start:
System.Web.Mvc.RazorViewEngine rve = (RazorViewEngine)ViewEngines.Engines
.Where(e=>e.GetType()==typeof(RazorViewEngine))
.FirstOrDefault();
string[] additionalPartialViewLocations = new[] {
"~/Views/[YourCustomPathHere]"
};
if(rve!=null)
{
rve.PartialViewLocationFormats = rve.PartialViewLocationFormats
.Union( additionalPartialViewLocations )
.ToArray();
}
Last I checked, this requires you to build your own ViewEngine. I don't know if they made it easier in RC1 though.
The basic approach I used before the first RC was, in my own ViewEngine, to split the namespace of the controller and look for folders which matched the parts.
EDIT:
Went back and found the code. Here's the general idea.
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
string ns = controllerContext.Controller.GetType().Namespace;
string controller = controllerContext.Controller.GetType().Name.Replace("Controller", "");
//try to find the view
string rel = "~/Views/" +
(
ns == baseControllerNamespace ? "" :
ns.Substring(baseControllerNamespace.Length + 1).Replace(".", "/") + "/"
)
+ controller;
string[] pathsToSearch = new string[]{
rel+"/"+viewName+".aspx",
rel+"/"+viewName+".ascx"
};
string viewPath = null;
foreach (var path in pathsToSearch)
{
if (this.VirtualPathProvider.FileExists(path))
{
viewPath = path;
break;
}
}
if (viewPath != null)
{
string masterPath = null;
//try find the master
if (!string.IsNullOrEmpty(masterName))
{
string[] masterPathsToSearch = new string[]{
rel+"/"+masterName+".master",
"~/Views/"+ controller +"/"+ masterName+".master",
"~/Views/Shared/"+ masterName+".master"
};
foreach (var path in masterPathsToSearch)
{
if (this.VirtualPathProvider.FileExists(path))
{
masterPath = path;
break;
}
}
}
if (string.IsNullOrEmpty(masterName) || masterPath != null)
{
return new ViewEngineResult(
this.CreateView(controllerContext, viewPath, masterPath), this);
}
}
//try default implementation
var result = base.FindView(controllerContext, viewName, masterName);
if (result.View == null)
{
//add the location searched
return new ViewEngineResult(pathsToSearch);
}
return result;
}
Try something like this:
private static void RegisterViewEngines(ICollection<IViewEngine> engines)
{
engines.Add(new WebFormViewEngine
{
MasterLocationFormats = new[] {"~/App/Views/Admin/{0}.master"},
PartialViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.ascx"},
ViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.aspx"}
});
}
protected void Application_Start()
{
RegisterViewEngines(ViewEngines.Engines);
}
Note: for ASP.NET MVC 2 they have additional location paths you will need to set for views in 'Areas'.
AreaViewLocationFormats
AreaPartialViewLocationFormats
AreaMasterLocationFormats
Creating a view engine for an Area is described on Phil's blog.
Note: This is for preview release 1 so is subject to change.
Most of the answers here, clear the existing locations by calling ViewEngines.Engines.Clear() and then add them back in again... there is no need to do this.
We can simply add the new locations to the existing ones, as shown below:
// note that the base class is RazorViewEngine, NOT WebFormViewEngine
public class ExpandedViewEngine : RazorViewEngine
{
public ExpandedViewEngine()
{
var customViewSubfolders = new[]
{
// {1} is conroller name, {0} is action name
"~/Areas/AreaName/Views/Subfolder1/{1}/{0}.cshtml",
"~/Areas/AreaName/Views/Subfolder1/Shared/{0}.cshtml"
};
var customPartialViewSubfolders = new[]
{
"~/Areas/MyAreaName/Views/Subfolder1/{1}/Partials/{0}.cshtml",
"~/Areas/MyAreaName/Views/Subfolder1/Shared/Partials/{0}.cshtml"
};
ViewLocationFormats = ViewLocationFormats.Union(customViewSubfolders).ToArray();
PartialViewLocationFormats = PartialViewLocationFormats.Union(customPartialViewSubfolders).ToArray();
// use the following if you want to extend the master locations
// MasterLocationFormats = MasterLocationFormats.Union(new[] { "new master location" }).ToArray();
}
}
Now you can configure your project to use the above RazorViewEngine in Global.asax:
protected void Application_Start()
{
ViewEngines.Engines.Add(new ExpandedViewEngine());
// more configurations
}
See this tutoral for more info.
I did it this way in MVC 5. I didn't want to clear the default locations.
Helper Class:
namespace ConKit.Helpers
{
public static class AppStartHelper
{
public static void AddConKitViewLocations()
{
// get engine
RazorViewEngine engine = ViewEngines.Engines.OfType<RazorViewEngine>().FirstOrDefault();
if (engine == null)
{
return;
}
// extend view locations
engine.ViewLocationFormats =
engine.ViewLocationFormats.Concat(new string[] {
"~/Views/ConKit/{1}/{0}.cshtml",
"~/Views/ConKit/{0}.cshtml"
}).ToArray();
// extend partial view locations
engine.PartialViewLocationFormats =
engine.PartialViewLocationFormats.Concat(new string[] {
"~/Views/ConKit/{0}.cshtml"
}).ToArray();
}
}
}
And then in Application_Start:
// Add ConKit View locations
ConKit.Helpers.AppStartHelper.AddConKitViewLocations();