Extending Sanderson's custom mvc ModelBinder for an object stored in session - asp.net-mvc

In his wonderful MVC book Steven Sanderson gives an example of a custom model binder that sets and retrieves a session variable, hiding the data storage element from the controller.
I'm trying to extend this to cater for a pretty common scenario: I'm storing a User object in the session and making this available to every action method as a parameter. Sanderson's class worked ok when the User details weren't changing, but now i need to let the user edit their details and save the amended object back to the session.
My problem is that I can't work out how to distinguish a GET from a POST other than by checking the number of keys in bindingContext.ValueProvider.Keys, and this seems so wrong I'm sure I'm misunderstanding something.
Can anyone point me in the right direction? Basically all Actions need access to the current user, and the UpdateMyDetails action needs to update that same object, all backed by the Session. Here's my code...
public class CurrentUserModelBinder : IModelBinder
{
private const string userSessionKey = "_currentuser";
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var user = controllerContext.HttpContext.Session[userSessionKey];
if (user == null)
throw new NullReferenceException("The CurrentUser was requested from the CurrentUserModelBinder but no IUser was present in the Session.");
var currentUser = (CCL.IUser)user;
if (bindingContext.ValueProvider.Keys.Count > 3)
{
var firstName = GetValue<string>(bindingContext, "FirstName");
if (string.IsNullOrEmpty(firstName))
bindingContext.ModelState.AddModelError("FirstName", "Please tell us your first name.");
else
currentUser.FirstName = firstName;
var lastName = GetValue<string>(bindingContext, "LastName");
if (string.IsNullOrEmpty(lastName))
bindingContext.ModelState.AddModelError("LastName", "Please tell us your last name.");
else
currentUser.LastName = lastName;
if (bindingContext.ModelState.IsValid)
controllerContext.HttpContext.Session[userSessionKey] = currentUser;
}
return currentUser;
}
private T GetValue<T>(ModelBindingContext bindingContext, string key)
{
ValueProviderResult valueResult;
bindingContext.ValueProvider.TryGetValue(key, out valueResult);
bindingContext.ModelState.SetModelValue(key, valueResult);
return (T)valueResult.ConvertTo(typeof(T));
}
}

Try inheriting from DefaultModelBinder instead of IModelBinder, then you can call base.BindModel to populate bindingContext.Model for mvc 1.0 or bindingContext.ModelMetadata.Model for mvc 2.0
To trigger bindingContext.Model to populate, call UpdateModel on the controller.
You need to add the statement from the book back in
if(bindingContext.Model != null)
throw new InvalidOperationException("Cannot update instances");
but change it to populate model and save on the session.
if(bindingContext.Model != null)
{
base.BindModel(controllerContext, bindingContext);
//save bindingContext.Model to session, overwriting current.
return bindingContext.Model
}

Related

How can I store a users preferences in MVC3

I have an MVC3 application and I would like to give the users the ability to set preferences that would be enabled when the user logs in.
I really don't have any idea where to start with this and would really appreciate being pointed in the right direction. I did try some changes to the membership class but now I am thinking that's probably not the best way to go about things.
You could do it in a database (sounds like you might be using one at least with the out-of-the-box membership provider) once uniquely identifying a user. In that case, you may want to implement your own membership provider.
You have to do a little work to start implementing your own provider. If this is your only requirement, you might be able to avoid it by writing your own class that returns settings in a format of your choosing
public static class UserSettings
{
public static string GetSettings(IPrincipal user)
{
if(user.Identity.IsAuthenticated)
{
// dip into database using user.Identity.Name property
return "string with user settings";
// this also assumes user.Identity.Name is uniquely able
// to identify a user in your database!
}
return string.Empty;
}
}
Or, if the information is completely trivial, maybe you could implement a cookie representation of the user settings. This, of course, comes with all the caveats of using cookies, but you could avoid storing the information in a database
Anywhere you have an HttpContext you could grab the settings value like so:
if(HttpContext.Current != null)
{
string userSettings = HttpRequest.Current.Request.Cookies["NameOfCookie"];
}
You can use the FormsAuthentication cookie to store your user information and avoid accessing the database all the time. That cookie is encrypted and whatever information you're storing as safe as the user session itself. The only problem with the cookies is that they have a maximum size of 4K so, if your user info is massive then you might run into a problem. When I use the cookie approach I store my user data as a JSON and then deserialize that JSON on each page request. Here is my login controller logic (I'm using SimpleMembership but the approach is the same:
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, model.RememberMe))
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
if (authTicket != null)
{
var user = _userLogic.GetItem(model.UserName);
if (user != null && user.IsActive)
{
var newAuthTicket = new FormsAuthenticationTicket(authTicket.Version, authTicket.Name, authTicket.IssueDate, authTicket.Expiration, authTicket.IsPersistent, JsonConvert.SerializeObject(user));
var newCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(newAuthTicket))
{
Expires = authCookie.Expires
};
Response.Cookies.Add(newCookie);
return RedirectToLocal(returnUrl);
}
WebSecurity.Logout();
ModelState.AddModelError("UserName", "This account has been deactivated.");
return View(model);
}
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
Notice the newAuthTicket creation and how user instance is passed to it as a JSON. After that all I have to do is desirialize this user object in my base controller's OnAuthorization method:
protected override void OnAuthorization(AuthorizationContext filterContext)
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
if (authTicket != null)
{
var principal = new CustomPrincipal(HttpContext.User.Identity)
{
CurrentUserInfo = JsonConvert.DeserializeObject<User>(authTicket.UserData)
};
HttpContext.User = principal;
AppUser = principal.CurrentUserInfo;
ViewBag.AppUser = AppUser;
}
}
base.OnAuthorization(filterContext);
}
Create a new table in your database.

How to check record ownership in controllers?

An application permits users to create records. For our purposes, let's call those records Goals.
One user should not be able to see Goals created by another user.
What is the best method for preventing UserA from accessing UserB's Goal?
I can do it like this:
//using asp.net membership
Guid uId = (Guid)System.Web.Security.Membership.GetUser().ProviderUserKey;
//goal records contain a foreign key to users, so I know who owns what
Goal theGoal = db.Goals.SingleOrDefault(g => g.GoalId == goalId
&& g.UserId == uId);
if (null == theGoal)
{
ViewData["error"] = "Can't find that goal.";
return View("Error");
}
else
{
return View(theGoal);
}
This works fine. The problem is that I've got similar code littered in every action that accesses goals.
Is there a more re-usable way of accomplishing this?
I thought of implementing it as an Authorization Filter. 2 problems with that scheme:
1) Requires the filter to know about and use the DB.
2) Requires 2 queries(1 in the filter, another in the action) instead of just the 1 query in the action that I have now.
What's a more DRY way of accomplishing this?
A custom model binder is a great place to do this:
public class GoalModelBinder : DefaultModelBinder
{
private readonly IGoalRepository _repository;
public GoalModelBinder(IGoalRepository repository)
{
_repository = repository;
}
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
// Here the default model binder does his job of binding stuff
// like the goal id which you would use in the repository to check
var goal = base.CreateModel(controllerContext, bindingContext, modelType) as Goal;
var user = controllerContext.HttpContext.User;
var theGoal = _repository.GetGoal(user, goal);
if (theGoal == null)
{
throw new HttpException(403, "Not authorized");
}
// It's OK, we've checked that the Goal belongs to the user
// => return it
return theGoal;
}
}
and then in your Application_Start register this model binder:
// some implementation of your repo
var sqlRepo = new SqlGoalRepository();
ModelBinders.Binders.Add(typeof(Goal), new GoalModelBinder(sqlRepo));
Now your controller action becomes less littered:
[Authorize]
public ActionResult Edit(Goal goal)
{
// if we get that far we are fine => we've got our goal
// and we are sure that it belongs to the currently logged user
return View(goal);
}

How to return an overall Model's value as 'null' from ASP.NET MVC model binder

I have an action method that takes several optional parameters.
This ASP.NET MVC actionmethod looks simple enough but isn't working as I want....
[HttpPost]
public ActionResult UpdateOrder(OrderItem OrderItem, Address ShippingAddress)
{
if (ShippingAddress != null) {
// we have a shipping address
}
}
An Address object is always created for ShippingAddress because - well - thats the way model binders work. Even if ShippingAddress.Address1, ShippingAddress.City etc. fields are absent from the Form an object will still be created and passed to the action.
I want a way to make a model binder that returns null for the model if it is deemed to be empty.
A first attempt goes as follows
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
// get the address to validate
var address = (Address)bindingContext.Model;
// if the address is quintessentially null then return null for the model binder
if (address.Address1 == null && address.CountryCode == null && address.City == null)
{
bindingContext.Model = null;
}
}
Unfortunately this simple solution doesn't work and I get the following error:
InvalidOperationException -This property setter is obsolete, because its value is derived from ModelMetadata.Model now.
Is there a way I can make the overall 'Model' from a custom ModelBinder to return null?
Have you tried setting the default parameter to null? You may also need to set the type to nullable as well, but I'm not 100% sure if it's needed, but that's how I use it.
For example:
public ActionResult UpdateOrder(OrderItem OrderItem, Address? shippingAddress = null)
I should probably note that this requires .NET 4, but then, you didn't specify which version you're running on.

ASP.NET model binding to base type

I have a BaseViewModel that my View Models all inherit from.
public class MagazineViewModel : BaseOutputViewMode
{
public string TitleOfPublication { get; set; }
}
In my controller I use a factory method to give the corret View Model back based on an input:
// e.g. viewModel contains an instance of MagazineViewModel
BaseOutputViewModel viewModel = BaseOutputViewModel.GetOutputViewModel(output);
When I use TryUpdateModel to try and bind to a FormCollection which I know contains a "TitleOfPublication" key, its never set in my view model:
if (!TryUpdateModel(viewModel, form))
I think this is something to do with the DefaultModelBinder using the BaseOutputViewModel to bind FormCollection keys to - it doesn't contain a "TitleOfPublication", the derived MagazineViewModel does.
I'm trying to roll my own model binder, to override the DefaultModelBinder's BindModel behavior. Its all wired in correctly and I can debug into it straight after the TryUpdateModel call:
public class TestModelBinder : DefaultModelBinder, IFilteredModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Tried the following without success ....
// 1. Quick hardcoded test
// bindingContext.ModelType = typeof(MagazineViewModel);
// 2. Set ModelMetadata, hardcoded test again
// bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(MagazineViewModel));
// 3. Replace the entire context
// ModelBindingContext context2 = new ModelBindingContext();
// context2.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(MagazineViewModel));
// context2.ModelName = bindingContext.ModelName;
// context2.ModelState = bindingContext.ModelState;
// context2.ValueProvider = bindingContext.ValueProvider;
// bindingContext = context2;
}
}
But I'm not sure how to work with the bindingContext? What needs to be updated so that I can tell the DefaultModelBinder to bind using the derived View Model properties?
Or have I just totally mis-understood this!
I did try overriding CreateModel - much like the DerivedTypeModelBinder in MvcContrib, but I think because I'm giving the binder an instance of a model to work with, CreateModel is never called. Used Reflector on the Mvc DLL, theres a "BindComplexModel" that calls CreateModel only if the model is null:
if (model == null)
{
model = this.CreateModel(controllerContext, bindingContext, modelType);
}
Any pointers greatfully received!
Cheers
OK - finally got to the bottom of this!
In actual fact there was nothing wrong with my model binder, the problem ultimately led back to a couple of input tags that had no name/id:
<input id="" name="" type="text">
The crux was this test in DefaultModelBinder:
// Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
// or by seeing if a value in the request exactly matches the name of the model we're binding.
// Complex type = everything else.
if (!performedFallback) {
ValueProviderResult vpResult =
bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (vpResult != null) {
return BindSimpleModel(controllerContext, bindingContext, vpResult);
}
}
With no id/name, the form collection has a key of "" which means that the GetValue correctly returned the value for that field, continuing to bind as a simple model.
When an id/name are added, the form collection contains no key of "", (which is now the name of my model as we're using TryUpdateModel). This meant the DefaultModelBinder correctly treated my model as complexm successfully binding properties in my derived type!
Cheers

Modelbinding database entities in ASPNET MVC

I'm having trouble trying to think what the best way is to recreate a database object in a controller Action.
I want to make use of ModelBinders so in my action I have access to the object via a parameter, rather than having to repeat code to get an object from the database based on an identifier parameter. So I was thinking of having a ModelBinder that performs a call to the dataaccess layer to obtain the original object (or creates a new one if it doesn't exist in the database), then binds any properties to the database object to update it. However I've read that the ModelBinders shouldn't make database queries (first comment of this article).
If the ModelBinder shouldn't perform a database query (so just using the DefaultModelBinder) then what about database objects that have properties that are other db objects? These would never get assigned.
Saving an object after the user has edited it (1 or 2 properties are editable in the view) the ModelBinded object would be missing data, so saving it as it is would result in data in the database being overwritten with invalid values, or NOT-NULL constraints failing.
So, whats the best way to get an object in a controller action from the database bound with the form data posted back from the view?
Note im using NHibernate.
I get the model object from the database, then use UpdateModel (or TryUpdateModel) on the object to update values from the form parameters.
public ActionResult Update( int id )
{
DataContext dc = new DataContext();
MyModel model = dc.MyModels.Where( m => m.ID == id ).SingleOrDefault();
string[] whitelist = new string[] { "Name", "Property1", "Property2" };
if (!TryUpdateModel( model, whitelist )) {
... model error handling...
return View("Edit");
}
ViewData.Model = model;
return View("Show");
}
Unfortunately you don't have control over the construction of the model binder, so you can't inject any repository implementation.
You can reach out directly into a service locator to pull in your repository & fetch the item:
public class ProductBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext,
ModelBindingContext bindingContext, Type modelType)
{
if(modelType != typeof(Product))
return null;
var form = controllerContext.HttpContext.Request.Form;
int id = Int32.Parse(form["Id"]);
if(id == 0)
return base.CreateModel(controllerContext, bindingContext, modelType);
IProductRepository repository = ServiceLocator.Resolve<IProductRepository>();
return repository.Fetch(id);
}
}
You might even make this work for all of your entities if you can use a base class or interface that provides the Id of the class.
You'll have to set this up in Global.asax:
ModelBinders.Binders.Add(typeof(Product), new ProductBinder());
and then you can do this:
public ActionResult Save([Bind] Product product)
{
....
_repository.Save(product);
}
Let me first state that I don't recommend to access database from ModelBinders, as from perspective of Separation Of Concern ModelBinders should only be responsible of interpretting client request, obviously database is not.
If you dont want to repeat your self (DRY), use repositories/services
However if u really want to do it like that, then
In global.asax.cs Register a custom MyModelBinderProvider to MVC
ModelBinderProviders.BinderProviders.Add(new EntityModelBinderProvider
{
ConnectionString = "my connection string"
));
Cunstruct the custom ModelBinderProvider to contain database settings
public class EntityBinderProvider: IModelBinderProvider
{
public string ConnectionString { get; set; }
public IModelBinder GetBinder(Type modelType)
{
if (Is known entity)
return new EntityBinder(ConnectionString);
else
return null;
}
}
Follow further instructions from Ben Scheirman
You don't actually have to hit the database. Simply setting the Id of the objects will be enough to set the relationship up, but watch your cascades. Make sure your cascde settings won't update the related object as it will clear the values.

Resources