BlueCode
BlueCode

Reputation: 741

AllowHtml attribute without referencing MVC

We have separated our business logic layer and business objects into a completely separate project/assembly. Some properties of the models can contain HTML content. In front of the business logic, we have an ASP.NET MVC web application, where users can manage the business objects.

So, how can we indicate to the MVC model binder that we want to allow HTML content on (and only on) some specific properties, without referencing ASP.NET MVC in our business logic layer? Or, how can metadata be injected from another assembly without strong references?

Thank you.

Upvotes: 11

Views: 3816

Answers (4)

GregTheDev
GregTheDev

Reputation: 623

In the event this is still useful to someone: I had similar requirements however my classes were generated by Entity Framework database first and so the project had used the [MetadataType] attribute extensively.

I cobbled together pieces from this question and linked questions into this solution to allow the approach to work with Metadata classes that specify [AllowHtml] (or similar)

In your Entity Framework project, define an attribute:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class SkipRequestValidationAttribute : Attribute
{
}

Then in your metadata classes, assign this attribute to the relevant properties:

[MetadataType(typeof(ActivityLogMetadata))]
public partial class ActivityLog
{
}

public class ActivityLogMetadata
{
    [Required]
    [SkipRequestValidation]
    [Display(Name = "Body")]
    public string Body { get; set; }
}

Now, in your MVC project add this custom Model binder to look for these meta attributes.

public class MyModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var containerType = bindingContext.ModelMetadata.ContainerType;

        if (containerType != null)
        {
            /* Do we have a Metadata attribute class specified for the Type we're binding? */
            var metaRedirectInfo = containerType
                .GetCustomAttributes(typeof(MetadataTypeAttribute), true)
                .OfType<MetadataTypeAttribute>().FirstOrDefault();

            if (metaRedirectInfo != null)
            {
                /* If our Meta class has a definition for this property, check it */
                var thisProperty = metaRedirectInfo.MetadataClassType.GetProperty(bindingContext.ModelMetadata.PropertyName);

                if (thisProperty != null)
                {
                    var hasAttribute = thisProperty
                        .GetCustomAttributes(false)
                        .Cast<Attribute>()
                        .Any(a => a.GetType().IsEquivalentTo(typeof(SkipRequestValidationAttribute)));

                    /* If we have a SkipRequestValidation attribute, ensure this property isn't validated */
                    if (hasAttribute)
                        bindingContext.ModelMetadata.RequestValidationEnabled = false;
                }
            }
        }

        return base.BindModel(controllerContext, bindingContext);
    }
}

Finally in your MVC project startup method (Startup.cs for example), replace the default model binder:

ModelBinders.Binders.DefaultBinder = new MyModelBinder();

Upvotes: 0

cmour
cmour

Reputation: 598

I had to change BindModel to the following (this builds on Russ Cam's answer) in order to check the attribute on the actual property. I also looked at this answer for help:

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {            
        var holderType = bindingContext.ModelMetadata.ContainerType;
        if (holderType != null)
        {
            var propertyType = holderType.GetProperty(bindingContext.ModelMetadata.PropertyName);
            var attributes = propertyType.GetCustomAttributes(true);
            var hasAttribute = attributes
              .Cast<Attribute>()
              .Any(a => a.GetType().IsEquivalentTo(typeof(MyAllowHtmlAttribute)));
            if (hasAttribute)
            {
                bindingContext.ModelMetadata.RequestValidationEnabled = false;
            }
        }

        return base.BindModel(controllerContext, bindingContext);
    }

Upvotes: 8

Russ Cam
Russ Cam

Reputation: 125488

Implement your own IModelBinder and AllowHtmlAttribute - put the attribute in your core project and the IModelBinder in your MVC application.

public class MyAllowHtmlAttribute : Attribute
{
}

To implement the IModelBinder, simply inherit from DefaultModelBinder and add logic to turn off request validation based on the presence of your own AllowHtmlAttribute

public class MyBetterDefaultModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var allowHtmlAttribute = bindingContext.ModelType.GetCustomAttribute<MyAllowHtmlAttribute>();

        if (allowHtmlAttribute != null)
        {
            bindingContext.ModelMetadata.RequestValidationEnabled = false;
        }

        return base.BindModel(controllerContext, bindingContext);
    }
}

Then hook up your own ModelBinder in Application_Start (or other startup code)

ModelBinders.Binders.DefaultBinder = new MyBetterDefaultModelBinder();

This logic in the custom model binder is what the AllowHtmlAttribute in MVC does but you wouldn't be able to use that one easily as it is intrinsically tied to ModelMetadata in MVC.

Upvotes: 4

blowdart
blowdart

Reputation: 56490

The request validation concept that AllowHtml relies on, and the binding checks are specific to web requests. There's no separation of concerns here, they're intimately linked. So no, you can't use it without taking a reference on System.Web etc.

You rule out the (in my opinion) most correct option - View Models even though validation and binding is really a view model concept.

You can't have portable business objects with web specific binding and validation concepts.

Upvotes: 1

Related Questions