diff --git a/test/System.Web.Mvc.Test/Html/Test/MetadataOverrideScope.cs b/test/System.Web.Mvc.Test/Html/Test/MetadataOverrideScope.cs new file mode 100644 index 00000000..3cbbb0fd --- /dev/null +++ b/test/System.Web.Mvc.Test/Html/Test/MetadataOverrideScope.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Moq; + +namespace System.Web.Mvc.Html.Test +{ + /// + /// + /// A scope within which the for a single is overridden. Could be + /// used for example to ensure the metadata for includes an additional property or + /// has a non-null . + /// + /// + /// Notes: Does _not_ override the metadata for subclasses of the given . And callers should + /// override (likely, mock) the metadata of the containing when changing the metadata of a + /// property e.g. modifying . + /// + /// + public class MetadataOverrideScope : IDisposable + { + private static readonly DataAnnotationsModelMetadataProvider AnnotationsProvider = + new DataAnnotationsModelMetadataProvider(); + + private readonly ModelMetadataProvider _oldMetadataProvider; + private readonly ModelMetadata _metadata; + private readonly Type _modelType; + + public MetadataOverrideScope(ModelMetadata metadata) + { + if (metadata == null) + { + throw new ArgumentNullException("metadata"); + } + if (metadata.ModelType == null) + { + throw new ArgumentException("Need ModelType", "metadata"); + } + + _oldMetadataProvider = ModelMetadataProviders.Current; + _metadata = metadata; + _modelType = metadata.ModelType; + + // Mock a ModelMetadataProvider which delegates to the old one in most cases. No need to special-case + // GetMetadataForProperties() because product code uses it only within ModelMetadata.Properties and our + // metadata instance will call _oldMetadataProvider there. + var metadataProvider = new Mock(); + metadataProvider + .Setup(p => p.GetMetadataForProperties(It.IsAny(), It.IsAny())) + .Returns((object container, Type containerType) => + _oldMetadataProvider.GetMetadataForProperties(container, containerType)); + metadataProvider + .Setup(p => p.GetMetadataForType(It.IsAny>(), It.IsAny())) + .Returns((Func modelAccessor, Type modelType) => + _oldMetadataProvider.GetMetadataForType(modelAccessor, modelType)); + + // When metadata for _modelType is requested, then return a clone of the provided metadata instance. + // GetMetadataForProperty() is important because the static discovery methods (e.g. + // ModelMetadata.FromLambdaExpression) use it. + metadataProvider + .Setup(p => p.GetMetadataForType(It.IsAny>(), _modelType)) + .Returns((Func modelAccessor, Type modelType) => GetMetadataForType(modelAccessor, modelType)); + metadataProvider + .Setup(p => + p.GetMetadataForProperty(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns((Func modelAccessor, Type containerType, string propertyName) => + GetMetadataForProperty(modelAccessor, containerType, propertyName)); + + // Calls to GetMetadataForProperties for the modelType are incorrect because _metadata.Provider must + // reference _oldMetadataProvider and not this mock. + metadataProvider + .Setup(p => p.GetMetadataForProperty(It.IsAny>(), _modelType, It.IsAny())) + .Throws(); + + // Finally make our ModelMetadataProvider visible everywhere. + ModelMetadataProviders.Current = metadataProvider.Object; + } + + public void Dispose() + { + ModelMetadataProviders.Current = _oldMetadataProvider; + } + + private ModelMetadata GetMetadataForType(Func modelAccessor, Type modelType) + { + return CloneMetadata(modelAccessor); + } + + private ModelMetadata GetMetadataForProperty( + Func modelAccessor, + Type containerType, + string propertyName) + { + var propertyMetadata = + _oldMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName); + if (propertyMetadata == null) + { + return null; + } + + if (propertyMetadata.ModelType == _modelType) + { + return CloneMetadata(() => propertyMetadata.Model); + } + + return propertyMetadata; + } + + private ModelMetadata CloneMetadata(Func modelAccessor) + { + var cachedMetadata = _metadata as CachedDataAnnotationsModelMetadata; + var annotationsMetadata = _metadata as DataAnnotationsModelMetadata; + ModelMetadata clonedMetadata; + if (cachedMetadata != null) + { + clonedMetadata = new CachedDataAnnotationsModelMetadata(cachedMetadata, modelAccessor); + } + else if (annotationsMetadata != null) + { + var provider = (_oldMetadataProvider as DataAnnotationsModelMetadataProvider) ?? AnnotationsProvider; + clonedMetadata = new DataAnnotationsModelMetadata( + provider, + annotationsMetadata.ContainerType, + modelAccessor, + _modelType, + annotationsMetadata.PropertyName, + displayColumnAttribute: null); // Copying SimpleDisplayText below compensates for null here. + } + else + { + clonedMetadata = new ModelMetadata( + _oldMetadataProvider, + _metadata.ContainerType, + modelAccessor, + _modelType, + _metadata.PropertyName); + } + + // Undo all the lazy-initialization of ModelMetadata and CachedDataAnnotationsModelMetadata... + clonedMetadata.Container = _metadata.Container; // May be incorrect. + clonedMetadata.ConvertEmptyStringToNull = _metadata.ConvertEmptyStringToNull; + clonedMetadata.DataTypeName = _metadata.DataTypeName; + clonedMetadata.Description = _metadata.Description; + clonedMetadata.DisplayFormatString = _metadata.DisplayFormatString; + clonedMetadata.DisplayName = _metadata.DisplayName; + clonedMetadata.EditFormatString = _metadata.EditFormatString; + clonedMetadata.HasNonDefaultEditFormat = _metadata.HasNonDefaultEditFormat; + clonedMetadata.HideSurroundingHtml = _metadata.HideSurroundingHtml; + clonedMetadata.HtmlEncode = _metadata.HtmlEncode; + clonedMetadata.IsReadOnly = _metadata.IsReadOnly; + clonedMetadata.IsRequired = _metadata.IsRequired; + clonedMetadata.NullDisplayText = _metadata.NullDisplayText; + clonedMetadata.Order = _metadata.Order; + clonedMetadata.RequestValidationEnabled = _metadata.RequestValidationEnabled; + clonedMetadata.ShortDisplayName = _metadata.ShortDisplayName; + clonedMetadata.ShowForDisplay = _metadata.ShowForDisplay; + clonedMetadata.ShowForEdit = _metadata.ShowForEdit; + clonedMetadata.SimpleDisplayText = _metadata.SimpleDisplayText; + clonedMetadata.TemplateHint = _metadata.TemplateHint; + clonedMetadata.Watermark = _metadata.Watermark; + foreach (var keyValuePair in _metadata.AdditionalValues) + { + clonedMetadata.AdditionalValues.Add(keyValuePair.Key, keyValuePair.Value); + } + + return clonedMetadata; + } + } +} diff --git a/test/System.Web.Mvc.Test/Html/Test/TemplateHelpersSafeScope.cs b/test/System.Web.Mvc.Test/Html/Test/TemplateHelpersSafeScope.cs new file mode 100644 index 00000000..0ea6b896 --- /dev/null +++ b/test/System.Web.Mvc.Test/Html/Test/TemplateHelpersSafeScope.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Web.WebPages.Scope; +using Moq; + +namespace System.Web.Mvc.Html.Test +{ + /// + /// A scope within which it is safe to invoke methods. For example + /// invokes ViewEngines.Engines.FindPartialView() and + /// clones the current . + /// + /// Similar to TemplateHelpersTest.MockViewEngine but FindPartialView() succeed there and fail here. In + /// addition TemplateHelpersTest tests do not continue far enough to need the transient scope. + /// + public class TemplateHelpersSafeScope : IDisposable + { + private readonly List _oldEngines; + private IDisposable _transientScope; + + public TemplateHelpersSafeScope() + { + // Copying an HtmlHelper instance reads and writes the current StorageScope. + // Ensure that's not the global scope. + _transientScope = ScopeStorage.CreateTransientScope(); + + // Do not want templates to check disk for anything. + var engine = new Mock(MockBehavior.Strict); + engine + .Setup(e => e.FindPartialView(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new ViewEngineResult(Enumerable.Empty())); + + _oldEngines = ViewEngines.Engines.ToList(); + ViewEngines.Engines.Clear(); + ViewEngines.Engines.Add(engine.Object); + } + + public void Dispose() + { + ViewEngines.Engines.Clear(); + foreach (var oldEngine in _oldEngines) + { + ViewEngines.Engines.Add(oldEngine); + } + + using (_transientScope) + { + _transientScope = null; + } + } + } +} diff --git a/test/System.Web.Mvc.Test/System.Web.Mvc.Test.csproj b/test/System.Web.Mvc.Test/System.Web.Mvc.Test.csproj index 4f0067d3..bc772f78 100644 --- a/test/System.Web.Mvc.Test/System.Web.Mvc.Test.csproj +++ b/test/System.Web.Mvc.Test/System.Web.Mvc.Test.csproj @@ -56,7 +56,9 @@ + +