Paperclip for asp.net mvc - asp.net-mvc

Is it exists kind of plugin like Paperclip for Rails?
it is really painful to implement own system for uploading files the resize it...
will be cool to have Attribute for model that will get params like so:
Model:
[Paperclip(Sizes={thumb="100x20",big="200x40"},Path="~/public/")]
public string Image{get;set;}
View:
Html.Editor(x=>x.Image)
here is small tutorial for rails.

Looks like this question is pretty old, I've stumbled across it by accident and because it doesn't seem to be answered yet, I decided to bring my 2¢.
I don't know if such plugin exists for ASP.NET but it would be trivially easy to write one:
[AttributeUsage(AttributeTargets.Property)]
public class PaperClipAttribute : Attribute, IMetadataAware
{
public PaperClipAttribute(string uploadPath, params string[] sizes)
{
if (string.IsNullOrEmpty(uploadPath))
{
throw new ArgumentException("Please specify an upload path");
}
UploadPath = uploadPath;
Sizes = sizes;
}
public string UploadPath { get; private set; }
public string[] Sizes { get; private set; }
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.AdditionalValues["PaperClip"] = this;
}
}
then define a custom model binder for the HttpPostedFileBase type:
public class PaperClipModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var paperClip = bindingContext.ModelMetadata.AdditionalValues["PaperClip"] as PaperClipAttribute;
if (paperClip == null)
{
return base.BindModel(controllerContext, bindingContext);
}
var uploadedFile = base.BindModel(controllerContext, bindingContext) as HttpPostedFileBase;
if (uploadedFile == null)
{
return null;
}
var uploadPath = controllerContext.HttpContext.Server.MapPath(paperClip.UploadPath);
if (!Directory.Exists(uploadPath))
{
throw new ArgumentException(string.Format("The specified folder \"{0}\" does not exist", uploadPath));
}
var sizes =
(from size in paperClip.Sizes
let tokens = size.Split('x')
select new Size(int.Parse(tokens[0]), int.Parse(tokens[1]))
).ToArray();
foreach (var size in sizes)
{
var extension = Path.GetExtension(uploadedFile.FileName);
var outputFilename = Path.Combine(
uploadPath,
Path.ChangeExtension(
string.Format("image{0}x{1}", size.Width, size.Height),
extension
)
);
Resize(uploadedFile.InputStream, outputFilename, size);
}
return base.BindModel(controllerContext, bindingContext);
}
private void Resize(Stream input, string outputFile, Size size)
{
using (var image = Image.FromStream(input))
using (var bmp = new Bitmap(size.Width, size.Height))
using (var gr = Graphics.FromImage(bmp))
{
gr.CompositingQuality = CompositingQuality.HighSpeed;
gr.SmoothingMode = SmoothingMode.HighSpeed;
gr.InterpolationMode = InterpolationMode.HighQualityBicubic;
gr.DrawImage(image, new Rectangle(0, 0, size.Width, size.Height));
bmp.Save(outputFile);
}
}
}
which will be registered in Application_Start:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders[typeof(HttpPostedFileBase)] = new PaperClipModelBinder();
}
and we are pretty much done. All that's left is classic stuff.
View model:
public class MyViewModel
{
[PaperClip("~/App_Data", "100x20", "200x40", "640x480")]
[Required(ErrorMessage = "Please select a file to upload")]
public HttpPostedFileBase Image { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel());
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
return Content("thanks for uploading");
}
}
View:
#model MyViewModel
#using (Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.LabelFor(x => x.Image)
<input type="file" name="image" />
#Html.ValidationMessageFor(x => x.Image)
<input type="submit" value="Upload" />
}

Related

Polymorphic model binding / Complex Models

I have problem in model binding. When I submit form it returns me id=0 and device is null? and how to solve it. My goal is to add new device, and choose device type from view by selector. if user selects smartphone it has to add fields for smartphone. I don't want to save device type in base class as Kind variable. Thanks in advance(sorry for english)
controller->
public IActionResult Index()
{
MainCont mainCont = new MainCont();
return View(mainCont);
}
index.cshtml ->
#model MainCont
#{
ViewData["Title"] = "Home Page";
}
<form action="home/create" method="post">
#Html.Partial("example",Model.Device)
<button type="submit">გაგზავნა</button>
</form>
example.cshtml ->
#model SmartPhone
#Html.TextBoxFor(model => model.imei)
#Html.TextBoxFor(model => model.screensize)
Device Model ->
public abstract class Device : Object
{
}
LaptopModel ->
public class Laptop : Device
{
public string CPU { get; set; }
public string GPu { get; set; }
}
MainCont ->
public class MainCont
{
public int Id{ get; set; }
public Device Device { get; set; }
}
SmartphoneModel ->
public class SmartPhone : Device
{
public string screensize { get; set; }
public string imei { get; set; }
}
model binder ->
using Bind.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Bind
{
public class DeviceModelBinder : IModelBinder
{
private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;
public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
{
this.binders = binders;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
IModelBinder modelBinder;
ModelMetadata modelMetadata;
if (bindingContext.ModelType == typeof(Laptop))
{
(modelMetadata, modelBinder) = binders[typeof(Laptop)];
}
else if (bindingContext.ModelType == typeof(SmartPhone))
{
(modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
bindingContext.ActionContext,
bindingContext.ValueProvider,
modelMetadata,
bindingInfo: null,
bindingContext.ModelName);
await modelBinder.BindModelAsync(newBindingContext);
bindingContext.Result = newBindingContext.Result;
if (newBindingContext.Result.IsModelSet)
{
// Setting the ValidationState ensures properties on derived types are correctly
bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
{
Metadata = modelMetadata,
};
}
}
}
}
binderprovider ->
using Bind.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Bind
{
public class DeviceModelBinderProvider: IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType != typeof(Device))
{
return null;
}
var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };
var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
foreach (var type in subclasses)
{
var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
}
return new DeviceModelBinder(binders);
}
}
}
Here is a demo:
Index.cshtml(when select SmartPhone,use example.cshtml,when select Laptop,use example1.cshtml):
#model MainCont
#{
ViewData["Title"] = "Home Page";
}
<form asp-action="create" asp-controller="home" method="post">
<select id="select" name="select">
<option value="SmartPhone">SmartPhone </option>
<option value="Laptop">Laptop </option>
</select>
<div id="sample"></div>
<button type="submit">გაგზავნა</button>
</form>
#section scripts{
<script>
$(function () {
GetPartialView();
})
$("#select").change(function () {
GetPartialView();
})
function GetPartialView() {
$.ajax({
url: "/Test1/ReturnExample",
type: "POST",
data: {
select: $("#select").val()
},
success: function (data) {
$('#sample').html(data);
},
error: function (reponse) {
alert("error : " + reponse);
}
});
}
</script>
}
example.cshtml:
#model SmartPhone
#Html.TextBoxFor(model => model.imei)
#Html.TextBoxFor(model => model.screensize)
example1.cshtml:
#model Laptop
#Html.TextBoxFor(model => model.CPU)
#Html.TextBoxFor(model => model.GPu)
Controller:
public IActionResult Index()
{
return View(new MainCont());
}
public IActionResult ReturnExample(string select)
{
if (select == "SmartPhone")
{
return PartialView("~/Views/Test1/example.cshtml", new SmartPhone());
}
else {
return PartialView("~/Views/Test1/example1.cshtml", new Laptop());
}
}
Create Action in Home Controller:
[HttpPost]
public IActionResult Create([ModelBinder(typeof(DataBinder))]MainCont mainCont) {
return Ok();
}
DataBinder:
public class DataBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var model1 = new MainCont();
var select = bindingContext.ValueProvider.GetValue("select").FirstValue;
if (select == "SmartPhone")
{
var model2 = new SmartPhone();
model2.screensize = bindingContext.ValueProvider.GetValue("screensize").FirstValue;
model2.imei = bindingContext.ValueProvider.GetValue("imei").FirstValue;
model1.Device = model2;
}
else if (select == "Laptop")
{
var model2 = new Laptop();
model2.CPU = bindingContext.ValueProvider.GetValue("CPU").FirstValue;
model2.GPu = bindingContext.ValueProvider.GetValue("GPu").FirstValue;
model1.Device = model2;
}
bindingContext.Result = ModelBindingResult.Success(model1);
return Task.CompletedTask;
}
}
result:

How to create ASP.NET MVC controller accepting unlimited amount of parameters from query string

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&param2=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.

How to validate uploaded file in ASP.NET MVC?

I have a Create action that takes an entity object and a HttpPostedFileBase image. The image does not belong to the entity model.
I can save the entity object in the database and the file in disk, but I am not sure how to validate these business rules:
Image is required
Content type must be "image/png"
Must not exceed 1MB
A custom validation attribute is one way to go:
public class ValidateFileAttribute : RequiredAttribute
{
public override bool IsValid(object value)
{
var file = value as HttpPostedFileBase;
if (file == null)
{
return false;
}
if (file.ContentLength > 1 * 1024 * 1024)
{
return false;
}
try
{
using (var img = Image.FromStream(file.InputStream))
{
return img.RawFormat.Equals(ImageFormat.Png);
}
}
catch { }
return false;
}
}
and then apply on your model:
public class MyViewModel
{
[ValidateFile(ErrorMessage = "Please select a PNG image smaller than 1MB")]
public HttpPostedFileBase File { get; set; }
}
The controller might look like this:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// The uploaded image corresponds to our business rules => process it
var fileName = Path.GetFileName(model.File.FileName);
var path = Path.Combine(Server.MapPath("~/App_Data"), fileName);
model.File.SaveAs(path);
return Content("Thanks for uploading", "text/plain");
}
}
and the view:
#model MyViewModel
#using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.LabelFor(x => x.File)
<input type="file" name="#Html.NameFor(x => x.File)" id="#Html.IdFor(x => x.File)" />
#Html.ValidationMessageFor(x => x.File)
<input type="submit" value="upload" />
}
Based on Darin Dimitrov's answer which I have found very helpful, I have an adapted version which allows checks for multiple file types, which is what I was initially looking for.
public override bool IsValid(object value)
{
bool isValid = false;
var file = value as HttpPostedFileBase;
if (file == null || file.ContentLength > 1 * 1024 * 1024)
{
return isValid;
}
if (IsFileTypeValid(file))
{
isValid = true;
}
return isValid;
}
private bool IsFileTypeValid(HttpPostedFileBase file)
{
bool isValid = false;
try
{
using (var img = Image.FromStream(file.InputStream))
{
if (IsOneOfValidFormats(img.RawFormat))
{
isValid = true;
}
}
}
catch
{
//Image is invalid
}
return isValid;
}
private bool IsOneOfValidFormats(ImageFormat rawFormat)
{
List<ImageFormat> formats = getValidFormats();
foreach (ImageFormat format in formats)
{
if(rawFormat.Equals(format))
{
return true;
}
}
return false;
}
private List<ImageFormat> getValidFormats()
{
List<ImageFormat> formats = new List<ImageFormat>();
formats.Add(ImageFormat.Png);
formats.Add(ImageFormat.Jpeg);
formats.Add(ImageFormat.Gif);
//add types here
return formats;
}
}
Here is a way to do it using viewmodel, take a look at whole code here
Asp.Net MVC file validation for size and type
Create a viewmodel as shown below with FileSize and FileTypes
public class ValidateFiles
{
[FileSize(10240)]
[FileTypes("doc,docx,xlsx")]
public HttpPostedFileBase File { get; set; }
}
Create custom attributes
public class FileSizeAttribute : ValidationAttribute
{
private readonly int _maxSize;
public FileSizeAttribute(int maxSize)
{
_maxSize = maxSize;
}
//.....
//.....
}
public class FileTypesAttribute : ValidationAttribute
{
private readonly List<string> _types;
public FileTypesAttribute(string types)
{
_types = types.Split(',').ToList();
}
//....
//...
}
And file length validation in asp.net core:
public async Task<IActionResult> MyAction()
{
var form = await Request.ReadFormAsync();
if (form.Files != null && form.Files.Count == 1)
{
var file = form.Files[0];
if (file.Length > 1 * 1024 * 1024)
{
ModelState.AddModelError(String.Empty, "Maximum file size is 1 Mb.");
}
}
// action code goes here
}
You may want to consider saving the image to database also:
using (MemoryStream mstream = new MemoryStream())
{
if (context.Request.Browser.Browser == "IE")
context.Request.Files[0].InputStream.CopyTo(mstream);
else
context.Request.InputStream.CopyTo(mstream);
if (ValidateIcon(mstream))
{
Icon icon = new Icon() { ImageData = mstream.ToArray(), MimeType = context.Request.ContentType };
this.iconRepository.SaveOrUpdate(icon);
}
}
I use this with NHibernate - entity defined:
public Icon(int id, byte[] imageData, string mimeType)
{
this.Id = id;
this.ImageData = imageData;
this.MimeType = mimeType;
}
public virtual byte[] ImageData { get; set; }
public virtual string MimeType { get; set; }
Then you can return the image as a FileContentResult:
public FileContentResult GetIcon(int? iconId)
{
try
{
if (!iconId.HasValue) return null;
Icon icon = this.iconRepository.Get(iconId.Value);
return File(icon.ImageData, icon.MimeType);
}
catch (Exception ex)
{
Log.ErrorFormat("ImageController: GetIcon Critical Error: {0}", ex);
return null;
}
}
Note that this is using ajax submit. Easier to access the data stream otherwise.

What's the best way to bind ExtJs 4 Grid filter info to asp.net mvc action parameters?

What's the best way to bind ExtJs 4 Grid filter info to asp.net mvc action parameters?
I wrote these helper classes:
public class ExtFilterInfo
{
public string Field { get; set; }
public ExtFilterData Data { get; set; }
}
public class ExtFilterData
{
public string Type { get; set; }
public string Value { get; set; }
}
Here is the Action:
public ActionResult Grid(int start, int limit, string sort, ExtFilterInfo[] filter)
The QueryString looks something like this:
_dc:1306799668564
filter%5B0%5D%5Bfield%5D:Nome
filter%5B0%5D%5Bdata%5D%5Btype%5D:string
filter%5B0%5D%5Bdata%5D%5Bvalue%5D:nu
page:1
start:0
limit:20
A custom model binder looks like it could fit the bill:
public class ExtFilterInfoModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var filter = (ExtFilterInfo)base.BindModel(controllerContext, bindingContext);
var field = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "[field]");
if (field != null)
{
filter.Field = field.AttemptedValue;
}
var type = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "[data][type]");
if (type != null)
{
if (filter.Data == null)
{
filter.Data = new ExtFilterData();
}
filter.Data.Type = type.AttemptedValue;
}
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "[data][value]");
if (value != null)
{
if (filter.Data == null)
{
filter.Data = new ExtFilterData();
}
filter.Data.Value = value.AttemptedValue;
}
return filter;
}
}
which could be registered in Application_Start:
ModelBinders.Binders.Add(typeof(ExtFilterInfo), new ExtFilterInfoModelBinder());
and now the filter collection which your controller action takes as argument should be bound correctly.

asp.net MVC 1.0 and 2.0 currency model binding

I would like to create model binding functionality so a user can enter ',' '.' etc for currency values which bind to a double value of my ViewModel.
I was able to do this in MVC 1.0 by creating a custom model binder, however since upgrading to MVC 2.0 this functionality no longer works.
Does anyone have any ideas or better solutions for performing this functionality? A better solution would be to use some data annotation or custom attribute.
public class MyViewModel
{
public double MyCurrencyValue { get; set; }
}
A preferred solution would be something like this...
public class MyViewModel
{
[CurrencyAttribute]
public double MyCurrencyValue { get; set; }
}
Below is my solution for model binding in MVC 1.0.
public class MyCustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = null;
ValueProviderResult valueResult;
bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out valueResult);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueResult);
if (bindingContext.ModelType == typeof(double))
{
string modelName = bindingContext.ModelName;
string attemptedValue = bindingContext.ValueProvider[modelName].AttemptedValue;
string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
string alternateSeperator = (wantedSeperator == "," ? "." : ",");
try
{
result = double.Parse(attemptedValue, NumberStyles.Any);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
}
else
{
result = base.BindModel(controllerContext, bindingContext);
}
return result;
}
}
You might try something among the lines:
// Just a marker attribute
public class CurrencyAttribute : Attribute
{
}
public class MyViewModel
{
[Currency]
public double MyCurrencyValue { get; set; }
}
public class CurrencyBinder : DefaultModelBinder
{
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
var currencyAttribute = propertyDescriptor.Attributes[typeof(CurrencyAttribute)];
// Check if the property has the marker attribute
if (currencyAttribute != null)
{
// TODO: improve this to handle prefixes:
var attemptedValue = bindingContext.ValueProvider
.GetValue(propertyDescriptor.Name).AttemptedValue;
return SomeMagicMethodThatParsesTheAttemptedValue(attemtedValue);
}
return base.GetPropertyValue(
controllerContext,
bindingContext, propertyDescriptor,
propertyBinder
);
}
}
public class HomeController: Controller
{
[HttpPost]
public ActionResult Index([ModelBinder(typeof(CurrencyBinder))] MyViewModel model)
{
return View();
}
}
UPDATE:
Here's an improvement of the binder (see TODO section in previous code):
if (!string.IsNullOrEmpty(bindingContext.ModelName))
{
var attemptedValue = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName).AttemptedValue;
return SomeMagicMethodThatParsesTheAttemptedValue(attemtedValue);
}
In order to handle collections you will need to register the binder in Application_Start as you will no longer be able to decorate the list with the ModelBinderAttribute:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(MyViewModel), new CurrencyBinder());
}
And then your action could look like this:
[HttpPost]
public ActionResult Index(IList<MyViewModel> model)
{
return View();
}
Summarizing the important part:
bindingContext.ValueProvider.GetValue(bindingContext.ModelName)
A further improvement step of this binder would be to handle validation (AddModelError/SetModelValue)

Resources