I want to have possibility to access action by the following URL type:
http://localhost/MyControllerName/MyActionName/Id1+Id2+Id3+Id4 etc.
and handle it in code in the following way:
public ActionResult MyActionName(string[] ids)
{
return View(ids);
}
+ is a reserved symbol in an url. It means white space. So to achieve what you are looking for you could write a custom model binder:
public class StringModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value != null && !string.IsNullOrEmpty(value.AttemptedValue))
{
return value.AttemptedValue.Split(' ');
}
return base.BindModel(controllerContext, bindingContext);
}
}
and then either register it globally for the string[] type or use the ModelBinder attribute:
public ActionResult MyActionName(
[ModelBinder(typeof(StringModelBinder))] string[] ids
)
{
return View(ids);
}
Obviously if you want to use an url of the form /MyControllerName/MyActionName/Id1+Id2+Id3+Id4 that will bind the last part as an action parameter called ids you will have to modify the default route definition which uses {id}.
After all chose the following solution:
public ActionResult Action(string id = "")
{
var ids = ParseIds(id);
return View(ids);
}
private static int[] ParseIds(string idsString)
{
idsString = idsString ?? string.Empty;
var idsStrings = idsString.Split(new[] { ' ', '+' });
var ids = new List<int>();
foreach (var idString in idsStrings)
{
int id;
if (!int.TryParse(idString, out id))
continue;
if (!ids.Contains(id))
ids.Add(id);
}
return ids.ToArray();
}
Related
I want a user to be able to query GET /api/mycontroller?enums=ABC
without using commas for the enums parameter. I know I can pass a comma separated parameter but using it without commas returns 'ABC' is not a valid value for type MyEnum. In my database, this field is stored as combination of characters without a comma. Is there a custom model binding attribute I can use and add it to the EnumVal property in MyRequest?
public enum MyEnum
{
A=1,
B=2,
C=4
}
public class MyRequest
{
public MyEnum EnumVal {get; set;}
}
[HttpGet("mycontroller")]
public async Task<ActionResult> MyController([FromQuery] MyRequest request)
{
//query db for row containing resuest.myEnum string combination...
// ...
}
I've looked into overriding the ValidationAttribute but it still returns an error response.
Fix the name of the action, since controller is a reserved word, you can not use it for the action name, and add enums input parameter
public async Task<ActionResult> My([FromQuery] MyRequest request, [FromQuery] string enums)
I was able to figure it out using a custom model binder
public class MyEnumTypeEntityBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
int len = valueProviderResult.FirstValue.Length;
string query = valueProviderResult.FirstValue;
char[] charlist = query.ToCharArray( );
string enumConversionString = string.Join(",", charlist);
if (!Enum.TryParse(enumConversionString, out MyEnum model))
{
bindingContext.ModelState.TryAddModelError(modelName, string.Format("{0} is not a valid value for type {1}", valueProviderResult.FirstValue, modelName));
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
and adding the attribute above the MyEnum request prop:
[ModelBinder(BinderType = typeof(MyEnumTypeEntityBinder))]
public MyEnum? Type { get; set; }
public enum MyEnum
{
A=1,
B=2,
C=4
}
I was wondering if there is an elegant way to add an array of complex types to a RouteValueDictionary or compatible type?
For example, if I have a class and an action:
public class TestObject
{
public string Name { get; set; }
public int Count { get; set; }
public TestObject()
{
}
public TestObject(string name, int count)
{
this.Name = name;
this.Count = count;
}
}
public ActionResult Test(ICollection<TestObjects> t)
{
return View();
}
then I know that if I call this action via the URL "/Test?t[0].Name=One&t[0].Count=1&t[1].Name=Two&t[1].Count=2" that MVC will map those query string parameters back into the ICollection type automatically. However, if I am manually creating a link somewhere using Url.Action(), and I want to pass a RouteValueDictionary of the parameters, when I add an ICollection to the RouteValueDictionary, Url.Action just renders it as the type, like &t=System.Collections.Generic.List.
For example:
RouteValueDictionary routeValDict = new RouteValueDictionary();
List<TestObject> testObjects = new List<TestObject>();
testObjects.Add(new TestObject("One", 1));
testObjects.Add(new TestObject("Two", 2));
routeValDict.Add("t", testObjects);
// Does not properly create the parameters for the List<TestObject> collection.
string url = Url.Action("Test", "Test", routeValDict);
Is there any way to get it to automatically render that collection into the format that MVC also understands how to map, or must I do this manually?
What am I missing, why would they make it so this beautiful mapping exists into an Action but not provide a way to manually work in the reverse direction for creating URLs?
I ran into this problem as well and used Zack's code but found a bug in it. If the IEnumerable is an array of string (string[]) then there is a problem. So i thaught I'd share my extended version.
public static RouteValueDictionary ToRouteValueDictionaryWithCollection(this RouteValueDictionary routeValues)
{
var newRouteValues = new RouteValueDictionary();
foreach(var key in routeValues.Keys)
{
object value = routeValues[key];
if(value is IEnumerable && !(value is string))
{
int index = 0;
foreach(object val in (IEnumerable)value)
{
if(val is string || val.GetType().IsPrimitive)
{
newRouteValues.Add(String.Format("{0}[{1}]", key, index), val);
}
else
{
var properties = val.GetType().GetProperties();
foreach(var propInfo in properties)
{
newRouteValues.Add(
String.Format("{0}[{1}].{2}", key, index, propInfo.Name),
propInfo.GetValue(val));
}
}
index++;
}
}
else
{
newRouteValues.Add(key, value);
}
}
return newRouteValues;
}
Well, I am open to other (more elegant) solutions, but I did get it working by taking the extension method found at this q/a: https://stackoverflow.com/a/5208050/1228414 and adapting it to use reflection for complex type properties instead of assuming primitive type arrays.
My code:
public static RouteValueDictionary ToRouteValueDictionaryWithCollection(this RouteValueDictionary routeValues)
{
RouteValueDictionary newRouteValues = new RouteValueDictionary();
foreach (var key in routeValues.Keys)
{
object value = routeValues[key];
if (value is IEnumerable && !(value is string))
{
int index = 0;
foreach (object val in (IEnumerable)value)
{
PropertyInfo[] properties = val.GetType().GetProperties();
foreach (PropertyInfo propInfo in properties)
{
newRouteValues.Add(
String.Format("{0}[{1}].{2}", key, index, propInfo.Name),
propInfo.GetValue(val));
}
index++;
}
}
else
{
newRouteValues.Add(key, value);
}
}
return newRouteValues;
}
I've seen a lot of similar posts on this, but haven't found the answer specific to controller parameters.
I've written a custom attribute called AliasAttribute that allows me to define aliases for parameters during model binding. So for example if I have: public JsonResult EmailCheck(string email) on the server and I want the email parameter to be bound to fields named PrimaryEmail or SomeCrazyEmail I can "map" this using the aliasattribute like this: public JsonResult EmailCheck([Alias(Suffix = "Email")]string email).
The problem: In my custom model binder I can't get a hold of the AliasAttribute class applied to the email parameter. It always returns null.
I've seen what the DefaultModelBinder class is doing to get the BindAttribute in reflector and its the same but doesn't work for me.
Question: How do I get this attribute during binding?
AliasModelBinder:
public class AliasModelBinder : DefaultModelBinder
{
public static ICustomTypeDescriptor GetTypeDescriptor(Type type)
{
return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var descriptor = GetTypeDescriptor(bindingContext.ModelType);
/*************************/
// this next statement returns null!
/*************************/
AliasAttribute attr = (AliasAttribute)descriptor.GetAttributes()[typeof(AliasAttribute)];
if (attr == null)
return null;
HttpRequestBase request = controllerContext.HttpContext.Request;
foreach (var key in request.Form.AllKeys)
{
if (string.IsNullOrEmpty(attr.Prefix) == false)
{
if (key.StartsWith(attr.Prefix, StringComparison.InvariantCultureIgnoreCase))
{
if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
return request.Form.Get(key);
}
}
else if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
if (attr.HasIncludes)
{
foreach (var include in attr.InlcludeSplit)
{
if (key.Equals(include, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(include);
}
}
}
}
return null;
}
}
AliasAttribute:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class AliasAttribute : Attribute
{
private string _include;
private string[] _inlcludeSplit = new string[0];
public string Prefix { get; set; }
public string Suffix { get; set; }
public string Include
{
get
{
return _include;
}
set
{
_include = value;
_inlcludeSplit = SplitString(_include);
}
}
public string[] InlcludeSplit
{
get
{
return _inlcludeSplit;
}
}
public bool HasIncludes { get { return InlcludeSplit.Length > 0; } }
internal static string[] SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
return (from piece in original.Split(new char[] { ',' })
let trimmed = piece.Trim()
where !string.IsNullOrEmpty(trimmed)
select trimmed).ToArray<string>();
}
}
Usage:
public JsonResult EmailCheck([ModelBinder(typeof(AliasModelBinder)), Alias(Suffix = "Email")]string email)
{
// email will be assigned to any field suffixed with "Email". e.g. PrimaryEmail, SecondaryEmail and so on
}
Gave up on this and then stumbled across the Action Parameter Alias code base that will probably allow me to do this. It's not as flexible as what I started out to write but probably can be modified to allow wild cards.
what I did was make my attribute subclass System.Web.Mvc.CustomModelBinderAttribute which then allows you to return a version of your custom model binder modified with the aliases.
example:
public class AliasAttribute : System.Web.Mvc.CustomModelBinderAttribute
{
public AliasAttribute()
{
}
public AliasAttribute( string alias )
{
Alias = alias;
}
public string Alias { get; set; }
public override IModelBinder GetBinder()
{
var binder = new AliasModelBinder();
if ( !string.IsNullOrEmpty( Alias ) )
binder.Alias = Alias;
return binder;
}
}
which then allows this usage:
public ActionResult Edit( [Alias( "somethingElse" )] string email )
{
// ...
}
I have a custom modelbinder, its check the authentication cookie and return the value.
public class UserDataModelBinder<T> : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.RequestContext.HttpContext.Request.IsAuthenticated)
{
var cookie =
controllerContext.RequestContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie == null)
return null;
var decrypted = FormsAuthentication.Decrypt(cookie.Value);
if (!string.IsNullOrWhiteSpace(decrypted.UserData))
return JsonSerializer.DeserializeFromString<T>(decrypted.UserData);
}
return null;
}
}
if I need to use it, I just need to pass it to the action. everything works.
public ActionResult Index(UserData userData)
{
AccountLoginWidgetVM model = new AccountLoginWidgetVM();
if (null != userData)
model.UserData = userData;
return View(userData);
}
However, I want to use it in my master page, because once user login, i want to display their info on the top on every page. I tried a few things, coudln't get it work
#Html.RenderPartial("LoginPartial", ???model here??)
We did it as follows:
Defined separate viewmodel for masterpages.
public class MasterPageViewModel
{
public Guid CurrentUserId { get; set; }
public string CurrentUserFullName { get; set; }
}
Added injection filter and filter provider.
public class MasterPageViewModelInjectorFilterProvider: IFilterProvider
{
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
return new [] {new Filter(new MasterPageViewModelInjectorFilter(), FilterScope.Action, null), };
}
private class MasterPageViewModelInjectorFilter: IResultFilter
{
public void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult == null)
return;
if (viewResult.ViewBag.MasterPageViewModel != null)
return;
//setup model whichever way you want
var viewModel = new MasterPageViewModel();
//inject model into ViewBag
viewResult.ViewBag.MasterPageViewModel = viewModel;
}
public void OnResultExecuted(ResultExecutedContext filterContext)
{
}
}
}
Configure filter provider:
//in Application_Start
FilterProviders.Providers.Add(new MasterPageViewModelInjectorFilterProvider());
Use in master:
ViewBag.MasterPageViewModel
This way you have fine uncoupled architecture. Of course you can combine it with Dependency Injection (we do, but I left it out for clarity) and configure your action filter for every action whichever way you want.
In this case you can use ViewBag.
public ActionResult Index(UserData userData)
{
AccountLoginWidgetVM model = new AccountLoginWidgetVM();
if (null != userData)
model.UserData = userData;
ViewBag.UserData = userData;
return View(userData);
}
#Html.RenderPartial("LoginPartial", ViewBag.UserData)
You have to make sure that userData is not null. If it'll be null the passed model will be default model of the view.
Example of URL
http_//host/url/unlimited/index?first=value1&second=value2...&anyvalidname=somevalue
I want to have one action accepting unknown in advance amount of params with unknown names. Something like this:
public class UnlimitedController : Controller
{
public ActionResult Index(object queryParams)
{
}
//or even better
public ActionResult Index(Dictionary<string, object> queryParams)
{
}
}
You could create a custom model binder that will convert the querystrings into dictionary.
Custom Model Binder
public class CustomModelBinder: IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var querystrings = controllerContext.HttpContext.Request.QueryString;
return querystrings.Cast<string>()
.Select(s => new { Key = s, Value = querystrings[s] })
.ToDictionary(p => p.Key, p => p.Value);
}
}
Action
public ActionResult Index([ModelBinder(typeof(CustomModelBinder))]
Dictionary<string, string> queryParams)
{
}
In HomeController.cs
public ActionResult Test()
{
Dictionary<string, string> data = new Dictionary<string, string>();
foreach (string index in Request.QueryString.AllKeys)
{
data.Add(index, Request.QueryString[index]);
}
StringBuilder sb = new StringBuilder();
foreach (var element in data)
{
sb.Append(element.Key + ": " + element.Value + "<br />");
}
ViewBag.Data = sb.ToString();
return View();
}
In Test.cshtml
<h2>Test</h2>
#Html.Raw(ViewBag.Data)
Webpage, http://localhost:35268/Home/Test?var1=1&var2=2, shows:
var1: 1
var2: 2
why dont you keep everything you want inside a single query string parameter and get it on server side as string
then parse the string urself and get what ever you want
something like this
http://example.com?a=someVar&b=var1_value1__var2_value2__var3_value3
then at server side just split the string and get the variables and all the values
if you dont want this then what you can do is that
just call the controller through the url and manually get into the Request.QueryString[] collection and you will get all the variables and there values there
Your controller code could be like
public ActionResult MultipleParam(int a, int b, int c)
{
ViewData["Output"] = a + b + c;
return View();
}
Global.asax.cs
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Parameter",
"{controller}/{action}/{a}/{b}/{c}",
new { controller = "Home", action = "MultipleParam", a = 0, b = 0, c = 0 }
);
}
If the route is {controller}/{action}/{id}/{page}, then /Home/MultipleParam/101/1?showComments=true, then the retrieval mechanism would be:
public ActionResult MultipleParam(string id /* = "101" */, int page /* = 1 */, bool showComments /* = true */) { }
Another possible solution is to create custom Route
public class ParamsEnabledRoute : RouteBase
{
private Route route;
public ParamsEnabledRoute(string url)
{
route = new Route(url, new MvcRouteHandler());
}
public override RouteData GetRouteData(HttpContextBase context)
{
var data = route.GetRouteData(context);
if (data != null)
{
var paramName = (string)data.Values["paramname"] ?? "parameters";
var parameters = context.Request.QueryString.AllKeys.ToDictionary(key => key, key => context.Request.QueryString[key]);
data.Values.Add(paramName, parameters);
return data;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext context, RouteValueDictionary rvd)
{
return route.GetVirtualPath(context, rvd);
}
}
Usage:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new ParamsEnabledRoute("ParamsEnabled/{controller}/{action}/{paramname}"));
}
Controller:
public class HomeController : Controller
{
public ActionResult Test(Dictionary<string, string> parameters)
{
}
}
URL:
http://localhost/ParamsEnabled/Home/Test/parameteres?param1=value1¶m2=value2
Route attribute:
public class RouteDataValueAttribute : ActionMethodSelectorAttribute
{
private readonly RouteDataValueAttributeEnum type;
public RouteDataValueAttribute(string valueName)
: this(valueName, RouteDataValueAttributeEnum.Required)
{
}
public RouteDataValueAttribute(string valueName, RouteDataValueAttributeEnum type)
{
this.type = type;
ValueName = valueName;
}
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
if (type == RouteDataValueAttributeEnum.Forbidden)
{
return controllerContext.RouteData.Values[ValueName] == null;
}
if (type == RouteDataValueAttributeEnum.Required)
{
return controllerContext.RouteData.Values[ValueName] != null;
}
return false;
}
public string ValueName { get; private set; }
}
public enum RouteDataValueAttributeEnum
{
Required,
Forbidden
}
Just use HttpContext to gather your query string.
using System.Web;
public class UnlimitedController : Controller
{
public ActionResult Index(object queryParams)
{
}
//or even better
public ActionResult Index()
{
NameValueCollection queryString = HttpContext.Request.QueryString;
// Access queryString in the same manner you would any Collection, including a Dictionary.
}
}
The question asked "How to create ASP.NET MVC controller accepting unlimited amount of parameters from query string"? Any controller will accept unlimited amount of parameters as a NamedValueCollection.