Skip to content

Instantly share code, notes, and snippets.

@restlessmedia
Last active August 24, 2016 07:55
Show Gist options
  • Save restlessmedia/da58972516182e6dd92e61480e593958 to your computer and use it in GitHub Desktop.
Save restlessmedia/da58972516182e6dd92e61480e593958 to your computer and use it in GitHub Desktop.
.Net Mvc bind abstract properties
using System;
namespace MvcApplication1
{
public class BindAsAttribute : Attribute
{
public BindAsAttribute(Type modelType)
{
ModelType = modelType;
}
public Type ModelType { get; private set; }
}
}
using MvcApplication1.Models;
using System;
using System.ComponentModel;
using System.Linq;
using System.Web.Mvc;
using System.Web;
using System.Collections.Generic;
namespace MvcApplication1
{
public class DefaultModelBinder : System.Web.Mvc.DefaultModelBinder
{
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
BindAsAttribute bindAs = GetBindAsAttribute(controllerContext, propertyDescriptor);
if (bindAs != null)
propertyDescriptor = BindAsProperty(bindingContext, propertyDescriptor, bindAs.ModelType);
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
protected virtual PropertyDescriptor BindAsProperty(ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, Type modelType)
{
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, modelType);
return GetPropertyDescriptor(propertyDescriptor, modelType);
}
private static PropertyDescriptor GetPropertyDescriptor(PropertyDescriptor propertyDescriptor, Type modelType)
{
if (!propertyDescriptor.PropertyType.IsAssignableFrom(modelType))
throw IncompatibleException(propertyDescriptor.PropertyType, modelType);
return new PropertyDescriptorWrapper(propertyDescriptor, modelType);
}
private static BindAsAttribute GetBindAsAttribute(ControllerContext controllerContext, PropertyDescriptor propertyDescriptor)
{
return propertyDescriptor.Attributes.OfType<BindAsAttribute>().FirstOrDefault();
}
private static Exception IncompatibleException(Type propertyType, Type modelType)
{
const string format = "The BindAs type {0} is incompatible with the property type {1}.";
return new InvalidOperationException(string.Format(format, modelType, propertyType));
}
private class PropertyDescriptorWrapper : PropertyDescriptor
{
public PropertyDescriptorWrapper(PropertyDescriptor descriptor, Type type)
: base(descriptor)
{
_descriptor = descriptor;
_type = type;
}
public override bool CanResetValue(object component)
{
return _descriptor.CanResetValue(component);
}
public override Type ComponentType
{
get { return _descriptor.ComponentType; }
}
public override object GetValue(object component)
{
return _descriptor.GetValue(component);
}
public override bool IsReadOnly
{
get { return _descriptor.IsReadOnly; }
}
public override Type PropertyType
{
get { return _type; }
}
public override void ResetValue(object component)
{
_descriptor.ResetValue(component);
}
public override void SetValue(object component, object value)
{
_descriptor.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return _descriptor.ShouldSerializeValue(component);
}
private readonly PropertyDescriptor _descriptor;
private readonly Type _type;
}
}
}
namespace MvcApplication1.Models
{
public class TestModel
{
[BindAs(typeof(Address))]
public IAddress Address { get; set; }
// Supports generics.
[BindAs(typeof(List<Address>))]
public IEnumerable<IAddress> { get; set; }
}
}
using System;
using System.ComponentModel;
using System.Linq;
using System.Web.Mvc;
namespace MvcApplication1
{
/// <summary>
/// Provides support for interface inheritance when accessing model properties
/// </summary>
/// <remarks>
/// Fixes bug where model property uses interface which inherits another interface. The previous provider would fail when accessing the property through razor.
/// Model
/// {
/// IFoo Foo;
/// }
/// IFoo
/// {
/// IBar Bar
/// }
/// i.e. TextBoxFor(m => m.Foo.Bar)
/// </remarks>
public class GenericModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
{
PropertyDescriptor property = GetPropertyDescriptor(containerType, propertyName);
Type containerTypeToUse = containerType;
if (property == null && containerType.IsInterface)
{
Tuple<PropertyDescriptor, Type> foundProperty = (
from t in containerType.GetInterfaces()
let p = GetTypeDescriptor(t).GetProperties().Find(propertyName, true)
where p != null
select (new Tuple<PropertyDescriptor, Type>(p, t))
).FirstOrDefault();
if (foundProperty != null)
{
property = foundProperty.Item1;
containerTypeToUse = foundProperty.Item2;
}
}
if (property == null)
throw CreateNotFoundException(containerType, propertyName);
return GetMetadataForProperty(modelAccessor, containerTypeToUse, property);
}
protected PropertyDescriptor GetPropertyDescriptor(Type containerType, string propertyName)
{
if (containerType == null)
throw new ArgumentNullException("containerType");
if (string.IsNullOrEmpty(propertyName))
throw new ArgumentException("The property {0} cannot be null or empty", "propertyName");
return GetTypeDescriptor(containerType).GetProperties().Find(propertyName, true);
}
protected Exception CreateNotFoundException(Type containerType, string propertyName)
{
throw new ArgumentException(string.Format("The property {0}.{1} could not be found", containerType.FullName, propertyName));
}
}
}
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Routing;
namespace MvcApplication1
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ModelMetadataProviders.Current = new GenericModelMetadataProvider();
ModelBinders.Binders.DefaultBinder = new DefaultModelBinder();
}
}
}
@restlessmedia
Copy link
Author

restlessmedia commented Aug 24, 2016

Allow the binding of abstract properties in Mvc.

  1. Add the GenericModelMetadataProvider.cs, DefaultModelBinder.cs & BindAsAttribute.cs files to your project.
  2. Override the DefaultBinder in Global.asax.cs with the new DefaultModelBinder. If you already override the DefaultBinder, ensure your class inherits the new one.
  3. Override the current ModelMetadataProvider in Global.asax.cs with the new GenericModelMetadataProvider, This is important as it circumvents a bug in Mvc where it's not possible to reference an inherited interface property in razor views.
  4. Decorate your abstract model properties with [BindAs(typeof(Concrete))] - see Example.cs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment