I upload an email through Ajax, parse it and then return some email properties as a partial view. If the user submits the form, the fields go to the post, however at this point I need to also retrieve the original file to store it in the database (so I won't store anything the user uploads, only relevant files). To do this, I store the File Stream (as byte[]) of the email in session. As there might be multiple pages open at the same time I save the emails in session as a list.
I then use the Validate event of the model to extract the byte[] FileStream from Session.
public class EmailModel
{
[Required]
[Display(Name = "TO")]
public string To { get; set; }
[Display(Name = "FROM")]
[Required]
public string From { get; set; }
[Display(Name = "Date")]
public DateTime Data { get; set; }
[Display(Name = "Location of requester")]
public string Location { get; set; }
[Required]
[Display(Name = "Subject of the mail ")]
public string SubjectMail { get; set; }
[Required]
[Display(Name = "Description ")]
public string EmailBodyAsText { get; set; }
public string EmailTypeAsString { get; set; }
public byte[] FileStream { get; set; }
public int ID { get; set; }
public string KendoUniqueId { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
FileStream = ExtractEmailFromSession(KendoUniqueId, HttpContext.Current.Session);
yield return null;
}
private byte[] ExtractEmailFromSession(string emailId, HttpSessionState session)
{
if (!string.IsNullOrEmpty(emailId))
{
var emailList = (List<EmailInSession>)session["emailInSession"];
return emailList.FirstOrDefault(x => x.EmailUniqueId == emailId)?.File;
}
else return null;
}
}
I think this is a bit of smelly code as I retrieve the content from an event that is supposed to be used for validating the data, so I am trying to figure out the proper place to do this. I know I can use IModelBinder to create a custom binder but then I need to bind all the properties which seems overkill for my purpose. Ideally I want to use a custom attribute only for the FileStream property that would extract the data from session and return it to the model. Is there such a possibility?
In case someone else comes to this thread in the future with a similar problem, please have a look below. Please bear in mind I could not make this really generic as this resolves a very specific situation. I created a Model Binder that inherits the default model binder. This allowed me to use the base BindModel method that did most of the job for me (the usual mapping of properties from the context that MVC does for us). Then I went for the specific property that I wanted to populate and extracted the data from the session.
public class SessionModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
try
{
var model = (EmailModel)base.BindModel(controllerContext, bindingContext);
model.FileStream = ExtractEmailFromSession(model.KendoUniqueId, HttpContext.Current.Session);
return model;
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError("","No data");
return null;
}
}
private byte[] ExtractEmailFromSession(string emailId, HttpSessionState session)
{
if (!string.IsNullOrEmpty(emailId))
{
var emailList = (List<EmailInSession>)session["emailInSession"];
return emailList?.FirstOrDefault(x => x.EmailUniqueId == emailId)?.File;
}
else return null;
}
}
Besides this, the new model binder needed to be registered. This is the place where you need to decide for which models your model binder applies.
public class SessionModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
if (modelType == typeof(EmailModel))
return new SessionModelBinder();
else return null;
}
}
Lastly, I needed to register my new binder provider so MVC knows about it. This is done by adding the line below in global asax (please note you need to put it in the chain at first position - 0, otherwise default model binder would kick in and yours will never be reached):
ModelBinderProviders.BinderProviders.Insert(0, new SessionModelBinderProvider());
Related
I am building a simple search, sort, page feature. I have attached the code below.
Below are the usecases:
My goal is to pass the "current filters" via each request to persist them particularly while sorting and paging.
Instead of polluting my action method with many (if not too many) parameters, I am thinking to use a generic type parameter that holds the current filters.
I need a custom model binder that can be able to achieve this.
Could someone please post an example implementation?
PS: I am also exploring alternatives as opposed to passing back and forth the complex objects. But i would need to take this route as a last resort and i could not find a good example of custom model binding generic type parameters. Any pointers to such examples can also help. Thanks!.
public async Task<IActionResult> Index(SearchSortPage<ProductSearchParamsVm> currentFilters, string sortField, int? page)
{
var currentSort = currentFilters.Sort;
// pass the current sort and sortField to determine the new sort & direction
currentFilters.Sort = SortUtility.DetermineSortAndDirection(sortField, currentSort);
currentFilters.Page = page ?? 1;
ViewData["CurrentFilters"] = currentFilters;
var bm = await ProductsProcessor.GetPaginatedAsync(currentFilters);
var vm = AutoMapper.Map<PaginatedResult<ProductBm>, PaginatedResult<ProductVm>>(bm);
return View(vm);
}
public class SearchSortPage<T> where T : class
{
public T Search { get; set; }
public Sort Sort { get; set; }
public Nullable<int> Page { get; set; }
}
public class Sort
{
public string Field { get; set; }
public string Direction { get; set; }
}
public class ProductSearchParamsVm
{
public string ProductTitle { get; set; }
public string ProductCategory { get; set; }
public Nullable<DateTime> DateSent { get; set; }
}
First create the Model Binder which should be implementing the interface IModelBinder
SearchSortPageModelBinder.cs
public class SearchSortPageModelBinder<T> : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
SearchSortPage<T> ssp = new SearchSortPage<T>();
//TODO: Setup the SearchSortPage<T> model
bindingContext.Result = ModelBindingResult.Success(ssp);
return TaskCache.CompletedTask;
}
}
And then create the Model Binder Provider which should be implementing the interface IModelBinderProvider
SearchSortPageModelBinderProvider.cs
public class SearchSortPageModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType.GetTypeInfo().IsGenericType &&
context.Metadata.ModelType.GetGenericTypeDefinition() == typeof(SearchSortPage<>))
{
Type[] types = context.Metadata.ModelType.GetGenericArguments();
Type o = typeof(SearchSortPageModelBinder<>).MakeGenericType(types);
return (IModelBinder)Activator.CreateInstance(o);
}
return null;
}
}
And the last thing is register the Model Binder Provider, it should be done in your Startup.cs
public void ConfigureServices(IServiceCollection services)
{
.
.
services.AddMvc(options=>
{
options.ModelBinderProviders.Insert(0, new SearchSortPageModelBinderProvider());
});
.
.
}
I am learning Asp.net MVC.
I am building an application in which I have following components:
1.Student class:
public class Students
{
[Required(ErrorMessage="Student Id is required")]
[Display(Name="Student Id")]
public int Sid { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Address { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
[DataType(DataType.Date)]
public DateTime DOB { get; set; }
[Required]
public string Email { get; set; }
}
2.StudentManager class
public List<Students> sList = new List<Students>()
{
new Students{Sid=1001,FirstName="A",LastName="T",DOB=Convert.ToDateTime("14-10-1987"),Email="a#b.com",Address="I"},
new Students{Sid=1002,FirstName="B",LastName="U",DOB=Convert.ToDateTime("14-10-1987"),Email="a#b.com",Address="I"},
new Students{Sid=1003,FirstName="C",LastName="V",DOB=Convert.ToDateTime("14-10-1987"),Email="a#b.com",Address="I"},
new Students{Sid=1004,FirstName="D",LastName="W",DOB=Convert.ToDateTime("14-10-1987"),Email="a#b.com",Address="I"},
new Students{Sid=1005,FirstName="E",LastName="X",DOB=Convert.ToDateTime("14-10-1987"),Email="a#b.com",Address="I"},
new Students{Sid=1006,FirstName="F",LastName="Y",DOB=Convert.ToDateTime("14-10-1987"),Email="a#b.com",Address="I"},
new Students{Sid=1007,FirstName="G",LastName="Z",DOB=Convert.ToDateTime("14-10-1987"),Email="a#b.com",Address="I"},
};
public void Edit(Students s)
{
Students stud = sList.Where(st => st.Sid == s.Sid).First();
stud.FirstName = s.FirstName;
stud.LastName = s.LastName;
stud.Email = s.Email;
stud.DOB = s.DOB;
}
3. And a student controller
public static StudentManager sm = new StudentManager();
// GET: Student
public ActionResult Index()
{
return View(sm.sList);
}
public ActionResult Edit(int? id)
{
Students student = sm.sList.Where(s => s.Sid == id).First();
return View(student);
}
[HttpPost]
public ActionResult Edit(Students s)
{
sm.Edit(s);
return RedirectToAction("Index");
}
My queries are:
1.I am able to edit a student's details.But if I do not use the keyword static in Controller then it is not updating.
2.How Model Binding automatically passes Student object to HttpPost Edit()?
Can someone please explain?
Every time you create StudentManager class you are creating new collection of students.
When StudentManager is static it is created only once, so you ll use the same object for every call and modify collection in it.
When StudentManager is not static for every call you ll create new object with new collection, so your edit will modify collection within object that ll be thrown away with a next call (like view).
It's not a good practice to have references to dependencies as static properties. I would recommend to make you collection property static and initialize it only once for life cycle of application and later migrate to use db or file or a call, depending of what your goal is.
Model Binding uses one of the registered Model Binders, some are provided by default so for main types of data, like JSON, you don't have to to anything.
I would definitely recommend to read a good book about mvc, to get the mechanics behind it. As the good and the bad part about it is it works like "magic" =)
What is the benefit of creating a ModelBinder when using lib.web.mvc. ?
This example from 2011 does not use a ModelBinder
http://tpeczek.com/2011/03/jqgrid-and-aspnet-mvc-strongly-typed.html
public class ProductViewModel
{
#region Properties
public int Id { get; set; }
public string Name { get; set; }
[JqGridColumnSortingName("SupplierId")]
public string Supplier { get; set; }
[JqGridColumnSortingName("CategoryId")]
public string Category { get; set; }
[DisplayName("Quantity Per Unit")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public string QuantityPerUnit { get; set; }
[DisplayName("Unit Price")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public decimal? UnitPrice { get; set; }
[DisplayName("Units In Stock")]
[JqGridColumnAlign(JqGridColumnAligns.Center)]
public short? UnitsInStock { get; set; }
#endregion
#region Constructor
public ProductViewModel()
{ }
public ProductViewModel(Product product)
{
this.Id = product.Id;
this.Name = product.Name;
this.Supplier = product.Supplier.Name;
this.Category = product.Category.Name;
this.QuantityPerUnit = product.QuantityPerUnit;
this.UnitPrice = product.UnitPrice;
this.UnitsInStock = product.UnitsInStock;
}
#endregion
}
But the latest examples are using them
http://tpeczek.codeplex.com/SourceControl/latest#trunk/ASP.NET%20MVC%20Examples/jqGrid%20Examples/jqGrid/Models/ProductViewModel.cs
namespace jqGrid.Models
{
[ModelBinder(typeof(ProductViewModelBinder))]
public class ProductViewModel
{
#region Properties
public int? ProductID { get; set; }
public string ProductName { get; set; }
public int SupplierID { get; set; }
public int CategoryID { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public short UnitsInStock { get; set; }
#endregion
}
public class ProductViewModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ProductViewModel model = (ProductViewModel)base.BindModel(controllerContext, bindingContext);
if (controllerContext.HttpContext.Request.Params["id"] != "_empty")
model.ProductID = Convert.ToInt32(controllerContext.HttpContext.Request.Params["id"]);
model.SupplierID = Convert.ToInt32(controllerContext.HttpContext.Request.Params["Supplier"]);
model.CategoryID = Convert.ToInt32(controllerContext.HttpContext.Request.Params["Category"]);
model.UnitPrice = Convert.ToDecimal(controllerContext.HttpContext.Request.Params["UnitPrice"].Replace(".", CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator));
return model;
}
}
Model Binders provide one of the most convenient functions that MVC has to offer. Model Binders' main job is to convert HTML Query Strings to strong-types. Out of the box, MVC model binders do an excellent job, but you may have a strong-type that doesn't work with the default binders. In that case you can create your own. Or you can customize them for example, maybe the postback only contains a single string value that you want to return an entire class or even a viewmodel packed with stuff.
Couple of things to keep in mind with the default behavior are: 1) MVC news up instances on it's own of Action Methods that take a first parameter of a model or view model class! 2) It will then attempt to populate that new instance with data returned from the Web Form (using Name/Value pairs) of the Query string. 3) Validation of the object (fields) happens before the first line in the controller is executed. 4) MVC model binding will NOT throw an error if there is missing field data (make sure your posted form fields have everything you need.
Finally with the functionality described above you can go a long way without writing custom binders. However they are there to handle the edge cases or any "tricks" you want to implement to make your application lean and mean. For me, I almost always use strongly-typed views and view-models as MVC does a great job of supporting full view binding to view models.
What is the right way to use (Try)UpdateModel?
When I run this:
TryUpdateModel returns true,
ViewData has no errors,
but my Proxy is not updated.
Action Method
public void Save(string TypeName, int Id, FormCollection idontknow) {
var types = Assembly.GetExecutingAssembly().GetTypes();
var ObjectType=(from t in types where t.Name == TypeName select t).First();
var Proxy = context.Set(ObjectType).Find(Id); // EF 4.1
if (TryUpdateModel(Proxy, TypeName)) {
var x = ViewData.GetModelStateErrors(); // no errors
}
}
Posted Data
TypeName=Thing&Id=1&Thing.Id=1&Thing.Name=hello&Thing.OptionID=2
Thing Class
public class Thing : Base {
public virtual Nullable<int> OptionID { get; set; }
public virtual Option Option { get; set; }
public virtual ICollection<ListItem> ListItems { get; set; }
}
public class Base {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
[NotMapped]
public virtual int? EntityState { get; set; }
}
EDIT: I also tried passing the form collection explicitly
TryUpdateModel(Proxy, TypeName, idontknow)
EDIT #2: (in response to NickLarsen)
Restarted VS and server, no change.
Values are actually in the FormCollection.
Mock data works! I know I must be messing up something here.
Using debugger to check values.
I stripped all the EF stuff and tried to get just that query string to populate the model with the values... and it worked just fine.
//controller class
public ActionResult Save(string TypeName, int Id, FormCollection idontknow)
{
var Proxy = new Thing
{
Id = 33,
OptionID = 2234,
Name = "tony",
};
if (TryUpdateModel(Proxy, TypeName))
{
ViewBag.Message = "WInner";
}
return RedirectToAction("Index");
}
//end controller class
public class Thing : Base
{
public virtual Nullable<int> OptionID { get; set; }
}
public class Base
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
Honestly I can't figure think of what in your code would keep it from working, but I would suggest going through the list 1 by one and testing after each step...
Save your progress and restart VS and your development server
Check that the values are actually in the form data, maybe something is getting in the way there.
Mock up some trash data like I did. (checking if the problem has something to do with EF)
How are you identifying that Proxy isn't being updated? In the debugger, on the page, etc?
Edit your question with the answer to all of the above questions.
so according to Gu IValidatableObject.Validate() should get called when a controller validates it's model (i.e. before ModelState.IsValid) however simply making the model implement IValidatableObject doesn't seem to work, because Validate(..) doesn't get called.
Anyone know if there is something else I have to wire up to get this to work?
EDIT:
Here is the code as requested.
public class LoginModel : IValidatableObject
{
[Required]
[Description("Email Address")]
public string Email { get; set; }
[Required]
[Description("Password")]
[DataType(DataType.Password)]
public string Password { get; set; }
[DisplayName("Remember Me")]
public bool RememberMe { get; set; }
public int UserPk { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var result = DataContext.Fetch( db => {
var user = db.Users.FirstOrDefault(u => u.Email == Email);
if (user == null) return new ValidationResult("That email address doesn't exist.");
if (user.Password != User.CreateHash(Password, user.Salt)) return new ValidationResult("The password supplied is incorrect.");
UserPk = user.UserPk;
return null;
});
return new List<ValidationResult>(){ result };
}
}
The action. ( I don't do anything special in the Controller...)
[HttpPost]
public ActionResult Login(LoginModel model)
{
if (ModelState.IsValid)
{
FormsAuthentication.SetAuthCookie(model.Email, model.RememberMe);
return Redirect(Request.UrlReferrer.AbsolutePath);
}
if (ControllerContext.IsChildAction || Request.IsAjaxRequest())
return View("LoginForm", model);
return View(model);
}
I set a break point on the first line of LoginModel.Validate() and it doesn't seem to get hit.
There isn't anything more than that you just have to add it to the model you're validating. Here's an example of validation
public class User : IValidatableObject {
public Int32 UserID { get; set; }
public string Name { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
//do your validation
return new List<ValidationResult>();
}
}
And your controller would use this model
public ActionResult Edit(User user) {
if (ModelState.IsValid) {
}
}
Hope this helps. Other requirements are .net 4 and data annotations - which you obviously need jsut for ivalidatableobject. Post any issues and we'll see if we can't resolve them - like post your model and your controller...you might be missing something.
Validation using the DefaultModelBinder is a two stage process. First, Data Annotations are validated. Then (and only if the data annotations validation resulted in zero errors), IValidatableObject.Validate() is called. This all takes place automatically when your post action has a viewmodel parameter. ModelState.IsValid doesn't do anything as such. Rather it just reports whether any item in the ModelState collection has non-empty ModelErrorCollection.