20 Jun 2012

A custom ValueInjecter Injection to use with models decorated with the MVC Bind Attribute

In some cases, I would like to use the Omu ValueInjecter's InjectFrom() to only copy source properties which have been marked for inclusion by an MVC BindAttribute. So, I've developed the following Injection:

public class BindAwareInjection : LoopValueInjection
{
    public List<string> PropertiesToInclude = new List<string>();
    public List<string> PropertiesToExclude = new List<string>();
    public BindRuleLocationType BindRuleLocation = BindRuleLocationType.Source;

    public enum BindRuleLocationType
    {
        Source,
        Target
    }

    protected override void Inject(object source, object target)
    {
        var bindRuleObject = BindRuleLocation == BindRuleLocationType.Source ? source : target;

        var bindAttributes = (BindAttribute[])bindRuleObject.GetType().GetCustomAttributes(typeof(BindAttribute), true);
        foreach (var bindRule in bindAttributes)
        {
            if (!string.IsNullOrEmpty(bindRule.Include))
            {
                PropertiesToInclude = new List<string>(bindRule.Include.Split(','));
            }
            else if (!string.IsNullOrEmpty(bindRule.Exclude))
            {
                PropertiesToExclude.AddRange(bindRule.Exclude.Split(','));
            }
        }

        base.Inject(source, target);
    }

    protected override bool UseSourceProp(string sourcePropName)
    {
        if (PropertiesToInclude.Count > 0)
        {
            return PropertiesToExclude.Any(prop => prop == sourcePropName);
        }
        else if (!PropertiesToExclude.Any(prop => prop == sourcePropName))
        {
            return true;
        }
        return false;
    }
}

This allows me to simply define ViewModels with Bind Exclusion lists like this:

public class User{
    public bool IsSuperUser { get; set; }
    public string Name { get; set; }
}

[Bind(Exclude = "IsSuperUser")]
public class UserVM : User { }

And then my Controller's Update Action would look like this:

[HttpPost]
public ActionResult UpdateUser(UserVM model)
{
    User target = Repository.GetCurrentUser();
    target.InjectFrom<BindAwareInjection>(model);
    return View(target);
}

Using that technique, the end user can update the User object's Name property without being able to overwrite the IsSuperUser property.

NOTE

It's true that MVC's UpdateModel() method obeys BindAttribute inclusion/exclusions found on the target object, so you may ask why we need to use ValueInjecter at all. However, that would necessitate the target object type to contain the Bind attributes. In our case, we want the source and target to be of different types, so that the Source (ViewModel) type has the Bind exclusions, leaving the Target type (Source's super-type) undecorated. This approach allows us to declare thin ViewModels based on underlying domain classes without having to slavishly modify our VM definitions every time the superclass definition changes.

Incidentally, by default the BindAwareInjection uses the Bind attributes from the Source, but you can use the Target attributes instead like this:

[HttpPost]
public ActionResult UpdateUser(UserVM model)
{
    var target = Repository.GetCurrentUser();
    var injection = new BindAwareInjection { BindRuleLocation = BindAwareInjection.BindRuleLocationType.Target };
    target.InjectFrom(injection, model);
    return View(target);
}

No comments:

Post a Comment

Comments are very welcome unless you're a spammer, in which case you should probably kill yourself.

If I helped you out today, you can buy me a beer below. Cheers!