ASP.NET MVC UpdateModel with interface
Asked Answered
H

2

6

I am trying to get UpdateModel to populate a model that is set as only an interface at compile-time. For example, I have:

// View Model
public class AccountViewModel {
  public string Email { get; set; }
  public IProfile Profile { get; set; }
}

// Interface
public interface IProfile {
  // Empty
}

// Actual profile instance used
public class StandardProfile : IProfile {
  public string FavoriteFood { get; set; }
  public string FavoriteMusic { get; set; }
}

// Controller action
public ActionResult AddAccount(AccountViewModel viewModel) {
  // viewModel is populated already
  UpdateModel(viewModel.Profile, "Profile"); // This isn't working.
}

// Form
<form ... >
  <input name='Email' />
  <input name='Profile.FavoriteFood' />
  <input name='Profile.FavoriteMusic' />
  <button type='submit'></button>
</form>

Also note that I have a custom model binder that inherits from DefaultModelBinder being used that populates IProfile with an instance of StandardProfile in the overriden CreateModel method.

The problem is that FavoriteFood and FavoriteMusic are never populated. Any ideas? Ideally this would all be done in the model binder, but I'm not sure it is possible without writing a completely custom implementation.

Thanks, Brian

Herdsman answered 9/9, 2009 at 16:30 Comment(0)
S
2

I would have to check the ASP.NET MVC code (DefaultModelBinder) but I'm guessing that its reflecting on the type IProfile, and not the instance, StandardProfile.

So it looks for any IProfile members it can try to bind, but its an empty interface, so it considers itself done.

You could try something like updating the BindingContext and changing the ModelType to StandardProfile and then calling

bindingContext.ModelType = typeof(StandardProfile);
IProfile profile = base.BindModel(controllerContext, bindingContext);

Anyways, having an empty Interface is weird~


Edit: just want to add that code above is just pseudo code, you would need to check DefaultModelBinder to see exactly what you want to write.


Edit#2:

Can you do:

public class ProfileModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
    {
        bindingContext.ModelType = typeof(StandardProfile);
        return base.BindModel(controllerContext, bindingContext);
    }
}

No need to make a model binder for AccountView, that one works fine.


Edit #3

Tested it out, the above binder works, just need to add:

ModelBinders.Binders[typeof(IProfile)] = new ProfileModelBinder();

Your action looks like:

public ActionResult AddAccount(AccountViewModel viewModel) {
    // viewModel is fully populated, including profile, don't call UpdateModel
}

You can use IOC when setting the model binder (have the type constructor injected for instance).

Saenz answered 9/9, 2009 at 17:2 Comment(5)
The empty interface allows for me to reuse the same core code code on each site I use this on, while supplying a different profile type using an IOC. I might be able to try a base class instead of an interface for this purpose, but I'm just unsure of what else I can do that gives me the flexibility I'm seeking. I'll look into the ModelType you've mentioned.Herdsman
An empty base class won't do you much good either. So in your code for the CreateModel method you are calling something like: IoC.GetInstance<IProfile>() which you have plugged to return a new StandardProfile? Interesting :). I'm still unclear how much code reuse you can get when anything that uses IProfile has to cast it to the right class first, but yah... I think specifying the Type in the binding context will work.Saenz
The reuse comes from the fact that many of my site's controllers are in a common assembly that I reference. Each site I build references this common assembly for controllers and models. I can then add additional controllers, models, views, etc specific for each site. In this case, I needed to be able to define entirely different profile fields on a site-by-site basis. Hence the need for just an interface here.Herdsman
Updated my answer. ModelBinders get called recursively, so you don't have to override the AccountModelViewModel ModelBinder, instead focus exactly on IProfile ModelBinder. You'll want to add for to the ModelBinder dictionary for IProfile. No idea if you can do that for interface (I'm not that smart) but worth a shot.Saenz
Just a quick remark: In version 2, the ModelType setter is now obsolete. You'll get this message if you try a set it: "This property setter is obsolete, because its value is derived from ModelMetadata.Model now."Jupiter
H
0

Not inspecting the actual type behind the interface was discussed here: http://forums.asp.net/t/1348233.aspx

That said, I found a hackish way around the problem. Since I already had a custom model binder for this type, I was able to add some code to it to perform the binding for me. Here's what my model binder looks like now:

public class AccountViewModelModelBinder : DefaultModelBinder
{
    private readonly IProfileViewModel profileViewModel;
    private bool profileBound = false;

    public AccountViewModelModelBinder(IProfileViewModel profileViewModel)
    {
        this.profileViewModel = profileViewModel;
    }

    protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // Bind the profile
        if (profileBound)
            return;

        profileBound = true;

        bindingContext.ModelType = profileViewModel.GetType();
        bindingContext.Model = profileViewModel;
        bindingContext.ModelName = "Profile";

        BindModel(controllerContext, bindingContext);
    }

    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
    {
        var model = new AccountViewModel();
        model.Profile = profileViewModel;

        return model;
    }
}

Basically, when the model binder is "done" binding the main AccountViewModel, I then alter the binding context (as suggested by eyston) and call BindModel once again. This then binds my profile. Note that I called GetType on the profileViewModel (which is supplied by the IOC container in the constructor). Also notice that I include a flag to indicate if the profile model has been bound already. Otherwise there would be an endless loop of OnModelUpdated being called.

I'm not saying this is pretty, but it does work well enough for my needs. I'd still love to hear about other suggestions.

Herdsman answered 9/9, 2009 at 19:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.