diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index deda4f5794..bb87abc83b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -245,6 +245,7 @@ + diff --git a/src/Umbraco.Tests/Web/Mvc/RenderModelBinderTests.cs b/src/Umbraco.Tests/Web/Mvc/RenderModelBinderTests.cs new file mode 100644 index 0000000000..5ec039a4dd --- /dev/null +++ b/src/Umbraco.Tests/Web/Mvc/RenderModelBinderTests.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.Tests.Web.Mvc +{ + [TestFixture] + public class RenderModelBinderTests + { + [Test] + public void Returns_Binder_For_IPublishedContent_And_IRenderModel() + { + var binder = new RenderModelBinder(); + var found = binder.GetBinder(typeof (IPublishedContent)); + Assert.IsNotNull(found); + found = binder.GetBinder(typeof(IRenderModel)); + Assert.IsNotNull(found); + found = binder.GetBinder(typeof(RenderModel)); + Assert.IsNotNull(found); + found = binder.GetBinder(typeof(DynamicPublishedContent)); + Assert.IsNotNull(found); + found = binder.GetBinder(typeof(MyContent)); + Assert.IsNotNull(found); + + found = binder.GetBinder(typeof(MyOtherContent)); + Assert.IsNull(found); + } + + [Test] + public void BindModel_Null_Source_Returns_Null() + { + Assert.IsNull(RenderModelBinder.BindModel(null, typeof(MyContent), CultureInfo.CurrentCulture)); + } + + [Test] + public void BindModel_Returns_If_Same_Type() + { + var content = new MyContent(Mock.Of()); + var bound = RenderModelBinder.BindModel(content, typeof (IPublishedContent), CultureInfo.CurrentCulture); + Assert.AreSame(content, bound); + } + + [Test] + public void BindModel_RenderModel_To_IPublishedContent() + { + var content = new MyContent(Mock.Of()); + var renderModel = new RenderModel(content, CultureInfo.CurrentCulture); + var bound = RenderModelBinder.BindModel(renderModel, typeof(IPublishedContent), CultureInfo.CurrentCulture); + Assert.AreSame(content, bound); + } + + [Test] + public void BindModel_IPublishedContent_To_RenderModel() + { + var content = new MyContent(Mock.Of()); + var bound = (IRenderModel)RenderModelBinder.BindModel(content, typeof(RenderModel), CultureInfo.CurrentCulture); + Assert.AreSame(content, bound.Content); + } + + [Test] + public void BindModel_IPublishedContent_To_Generic_RenderModel() + { + var content = new MyContent(Mock.Of()); + var bound = (IRenderModel)RenderModelBinder.BindModel(content, typeof(RenderModel), CultureInfo.CurrentCulture); + Assert.AreSame(content, bound.Content); + } + + [Test] + public void No_DataToken_Returns_Null() + { + var binder = new RenderModelBinder(); + var routeData = new RouteData(); + var result = binder.BindModel(new ControllerContext(Mock.Of(), routeData, Mock.Of()), + new ModelBindingContext()); + + Assert.IsNull(result); + } + + [Test] + public void Invalid_DataToken_Model_Type_Returns_Null() + { + var binder = new RenderModelBinder(); + var routeData = new RouteData(); + routeData.DataTokens[Core.Constants.Web.UmbracoDataToken] = "hello"; + + //the value provider is the default implementation + var valueProvider = new Mock(); + //also IUnvalidatedValueProvider + var invalidatedValueProvider = valueProvider.As(); + invalidatedValueProvider.Setup(x => x.GetValue(It.IsAny(), It.IsAny())).Returns(() => + new ValueProviderResult(null, "", CultureInfo.CurrentCulture)); + + var controllerCtx = new ControllerContext( + Mock.Of(http => http.Items == new Dictionary()), + routeData, + Mock.Of()); + + var result = binder.BindModel(controllerCtx, + new ModelBindingContext + { + ValueProvider = valueProvider.Object, + ModelMetadata = new ModelMetadata(new EmptyModelMetadataProvider(), null, () => null, typeof(IPublishedContent), "content") + }); + + Assert.IsNull(result); + } + + [Test] + public void IPublishedContent_DataToken_Model_Type_Uses_DefaultImplementation() + { + var content = new MyContent(Mock.Of()); + var binder = new RenderModelBinder(); + var routeData = new RouteData(); + routeData.DataTokens[Core.Constants.Web.UmbracoDataToken] = content; + + //the value provider is the default implementation + var valueProvider = new Mock(); + //also IUnvalidatedValueProvider + var invalidatedValueProvider = valueProvider.As(); + invalidatedValueProvider.Setup(x => x.GetValue(It.IsAny(), It.IsAny())).Returns(() => + new ValueProviderResult(content, "content", CultureInfo.CurrentCulture)); + + var controllerCtx = new ControllerContext( + Mock.Of(http => http.Items == new Dictionary()), + routeData, + Mock.Of()); + var result = binder.BindModel(controllerCtx, + new ModelBindingContext + { + ValueProvider = valueProvider.Object, + ModelMetadata = new ModelMetadata(new EmptyModelMetadataProvider(), null, () => null, typeof(IPublishedContent), "content") + }); + + Assert.AreEqual(content, result); + } + + public class MyOtherContent + { + + } + + public class MyContent : PublishedContentWrapped + { + public MyContent(IPublishedContent content) : base(content) + { + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RenderModelBinder.cs b/src/Umbraco.Web/Mvc/RenderModelBinder.cs index 32802ccadc..dd9af16140 100644 --- a/src/Umbraco.Web/Mvc/RenderModelBinder.cs +++ b/src/Umbraco.Web/Mvc/RenderModelBinder.cs @@ -8,7 +8,10 @@ using Umbraco.Web.Models; namespace Umbraco.Web.Mvc { - public class RenderModelBinder : IModelBinder, IModelBinderProvider + /// + /// Allows for Model Binding any IPublishedContent or IRenderModel + /// + public class RenderModelBinder : DefaultModelBinder, IModelBinder, IModelBinderProvider { /// /// Binds the model to a value by using the specified controller context and binding context. @@ -17,14 +20,29 @@ namespace Umbraco.Web.Mvc /// The bound value. /// /// The controller context.The binding context. - public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { object model; if (controllerContext.RouteData.DataTokens.TryGetValue(Core.Constants.Web.UmbracoDataToken, out model) == false) return null; - //default culture - var culture = CultureInfo.CurrentCulture; + //This model binder deals with IRenderModel and IPublishedContent by extracting the model from the route's + // datatokens. This data token is set in 2 places: RenderRouteHandler, UmbracoVirtualNodeRouteHandler + // and both always set the model to an instance of `RenderModel`. So if this isn't an instance of IRenderModel then + // we need to let the DefaultModelBinder deal with the logic. + var renderModel = model as IRenderModel; + if (renderModel == null) + { + model = base.BindModel(controllerContext, bindingContext); + if (model == null) return null; + } + + //if for any reason the model is not either IRenderModel or IPublishedContent, then we return since those are the only + // types this binder is dealing with. + if ((model is IRenderModel) == false && (model is IPublishedContent) == false) return null; + + //default culture + var culture = CultureInfo.CurrentCulture; var umbracoContext = controllerContext.GetUmbracoContext() ?? UmbracoContext.Current; @@ -34,8 +52,8 @@ namespace Umbraco.Web.Mvc culture = umbracoContext.PublishedContentRequest.Culture; } - return BindModel(model, bindingContext.ModelType, culture); - } + return BindModel(model, bindingContext.ModelType, culture); + } // source is the model that we have // modelType is the type of the model that we need to bind to @@ -140,15 +158,9 @@ namespace Umbraco.Web.Mvc public IModelBinder GetBinder(Type modelType) { - // can bind to RenderModel - if (modelType == typeof(RenderModel)) return this; - - // can bind to RenderModel - if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(RenderModel<>)) return this; - - // can bind to TContent where TContent : IPublishedContent - if (typeof(IPublishedContent).IsAssignableFrom(modelType)) return this; - return null; + return TypeHelper.IsTypeAssignableFrom(modelType) || TypeHelper.IsTypeAssignableFrom(modelType) + ? this + : null; } } } \ No newline at end of file