Tag Helpers: add `ModelExpression` class to support `Expression<Func<TModel, TValue>>` attributes

- includes new `RazorPage<TModel>.CreateModelExpression<TValue>()` method
- #1240

nit:
- regenerating the resources reordered Microsoft.AspNet.Mvc.Core's Resources.designer.cs
This commit is contained in:
Doug Bunting 2014-10-06 20:41:16 -07:00 коммит произвёл N. Taylor Mullen
Родитель 3290791c5f
Коммит 639a788ed8
6 изменённых файлов: 281 добавлений и 16 удалений

Просмотреть файл

@ -1482,22 +1482,6 @@ namespace Microsoft.AspNet.Mvc.Core
return GetString("AttributeRoute_NullTemplateRepresentation");
}
/// <summary>
/// "The path to the file must be absolute: {0}"
/// </summary>
internal static string FileResult_InvalidPathType_RelativeOrVirtualPath
{
get { return GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"); }
}
/// <summary>
/// "The path to the file must be absolute: {0}"
/// </summary>
internal static string FormatFileResult_InvalidPathType_RelativeOrVirtualPath(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"), p0);
}
/// <summary>
/// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
/// </summary>
@ -1514,6 +1498,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("DefaultActionSelector_AmbiguousActions"), p0, p1);
}
/// <summary>
/// "The path to the file must be absolute: {0}"
/// </summary>
internal static string FileResult_InvalidPathType_RelativeOrVirtualPath
{
get { return GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"); }
}
/// <summary>
/// "The path to the file must be absolute: {0}"
/// </summary>
internal static string FormatFileResult_InvalidPathType_RelativeOrVirtualPath(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

Просмотреть файл

@ -0,0 +1,47 @@

using System;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.Rendering
{
/// <summary>
/// Describes an <see cref="System.Linq.Expressions.Expression"/> passed to a tag helper.
/// </summary>
public class ModelExpression
{
/// <summary>
/// Initializes a new instance of the <see cref="ModelExpression"/> class.
/// </summary>
/// <param name="name">
/// String representation of the <see cref="System.Linq.Expressions.Expression"/> of interest.
/// </param>
/// <param name="metadata">
/// Metadata about the <see cref="System.Linq.Expressions.Expression"/> of interest.
/// </param>
public ModelExpression(string name, [NotNull] ModelMetadata metadata)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(name));
}
Name = name;
Metadata = metadata;
}
/// <summary>
/// String representation of the <see cref="System.Linq.Expressions.Expression"/> of interest.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Metadata about the <see cref="System.Linq.Expressions.Expression"/> of interest.
/// </summary>
/// <remarks>
/// Getting <see cref="ModelMetadata.Model"/> will evaluate a compiled version of the original
/// <see cref="System.Linq.Expressions.Expression"/>.
/// </remarks>
public ModelMetadata Metadata { get; private set; }
}
}

Просмотреть файл

@ -186,6 +186,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return GetString("RazorPage_YouCannotFlushWhileInAWritingScope");
}
/// <summary>
/// The {0} was unable to provide metadata for expression '{1}'.
/// </summary>
internal static string RazorPage_NullModelMetadata
{
get { return GetString("RazorPage_NullModelMetadata"); }
}
/// <summary>
/// The {0} was unable to provide metadata for expression '{1}'.
/// </summary>
internal static string FormatRazorPage_NullModelMetadata(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RazorPage_NullModelMetadata"), p0, p1);
}
/// <summary>
/// {0} can only be called from a layout page.
/// </summary>

Просмотреть файл

@ -1,6 +1,13 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq.Expressions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Rendering.Expressions;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
@ -9,6 +16,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <typeparam name="TModel">The type of the view data model.</typeparam>
public abstract class RazorPage<TModel> : RazorPage
{
IModelMetadataProvider _provider;
public TModel Model
{
get
@ -19,5 +28,33 @@ namespace Microsoft.AspNet.Mvc.Razor
[Activate]
public ViewDataDictionary<TModel> ViewData { get; set; }
/// <summary>
/// Returns a <see cref="ModelExpression"/> instance describing the given <paramref name="expression"/>.
/// </summary>
/// <typeparam name="TValue">The type of the <paramref name="expression"/> result.</typeparam>
/// <param name="expression">An expression to be evaluated against the current model.</param>
/// <returns>A new <see cref="ModelExpression"/> instance describing the given <paramref name="expression"/>.
/// </returns>
/// <remarks>
/// Compiler normally infers <typeparamref name="TValue"/> from the given <paramref name="expression"/>.
/// </remarks>
public ModelExpression CreateModelExpression<TValue>([NotNull] Expression<Func<TModel, TValue>> expression)
{
if (_provider == null)
{
_provider = Context.RequestServices.GetService<IModelMetadataProvider>();
}
var name = ExpressionHelper.GetExpressionText(expression);
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, _provider);
if (metadata == null)
{
throw new InvalidOperationException(
Resources.FormatRazorPage_NullModelMetadata(nameof(IModelMetadataProvider), name));
}
return new ModelExpression(name, metadata);
}
}
}

Просмотреть файл

@ -150,6 +150,9 @@
<data name="RazorPage_YouCannotFlushWhileInAWritingScope" xml:space="preserve">
<value>You cannot flush while inside a writing scope.</value>
</data>
<data name="RazorPage_NullModelMetadata" xml:space="preserve">
<value>The {0} was unable to provide metadata for expression '{1}'.</value>
</data>
<data name="RenderBodyCannotBeCalled" xml:space="preserve">
<value>{0} can only be called from a layout page.</value>
</data>

Просмотреть файл

@ -0,0 +1,162 @@

using System;
using System.IO;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
{
public class RazorPageCreateModelExpressionTest
{
public static TheoryData<Expression<Func<RazorPageCreateModelExpressionModel, int>>, string> IntExpressions
{
get
{
var somethingElse = 23;
return new TheoryData<Expression<Func<RazorPageCreateModelExpressionModel, int>>, string>
{
{ model => somethingElse, "somethingElse" },
{ model => model.Id, "Id" },
{ model => model.SubModel.Id, "SubModel.Id" },
{ model => model.SubModel.SubSubModel.Id, "SubModel.SubSubModel.Id" },
};
}
}
public static TheoryData<Expression<Func<RazorPageCreateModelExpressionModel, string>>, string> StringExpressions
{
get
{
var somethingElse = "This is something else";
return new TheoryData<Expression<Func<RazorPageCreateModelExpressionModel, string>>, string>
{
{ model => somethingElse, "somethingElse" },
{ model => model.Name, "Name" },
{ model => model.SubModel.Name, "SubModel.Name" },
{ model => model.SubModel.SubSubModel.Name, "SubModel.SubSubModel.Name" },
};
}
}
[Theory]
[MemberData(nameof(IntExpressions))]
public void CreateModelExpression_ReturnsExpectedMetadata_IntExpressions(
Expression<Func<RazorPageCreateModelExpressionModel, int>> expression,
string expectedName)
{
// Arrange
var viewContext = CreateViewContext(model: null);
var page = CreatePage(viewContext);
// Act
var result = page.CreateModelExpression(expression);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Metadata);
Assert.Equal(typeof(int), result.Metadata.ModelType);
Assert.Equal(expectedName, result.Name);
}
[Theory]
[MemberData(nameof(StringExpressions))]
public void CreateModelExpression_ReturnsExpectedMetadata_StringExpressions(
Expression<Func<RazorPageCreateModelExpressionModel, string>> expression,
string expectedName)
{
// Arrange
var viewContext = CreateViewContext(model: null);
var page = CreatePage(viewContext);
// Act
var result = page.CreateModelExpression(expression);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Metadata);
Assert.Equal(typeof(string), result.Metadata.ModelType);
Assert.Equal(expectedName, result.Name);
}
private static TestRazorPage CreatePage(ViewContext viewContext)
{
return new TestRazorPage
{
ViewContext = viewContext,
ViewData = (ViewDataDictionary<RazorPageCreateModelExpressionModel>)viewContext.ViewData,
};
}
private static ViewContext CreateViewContext(RazorPageCreateModelExpressionModel model)
{
return CreateViewContext(model, new DataAnnotationsModelMetadataProvider());
}
private static ViewContext CreateViewContext(
RazorPageCreateModelExpressionModel model,
IModelMetadataProvider provider)
{
var viewData = new ViewDataDictionary<RazorPageCreateModelExpressionModel>(provider)
{
Model = model,
};
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider
.Setup(real => real.GetService(typeof(IModelMetadataProvider)))
.Returns(provider);
var httpContext = new Mock<HttpContext>();
httpContext
.SetupGet(real => real.RequestServices)
.Returns(serviceProvider.Object);
var actionContext = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor());
return new ViewContext(
actionContext,
view: Mock.Of<IView>(),
viewData: viewData,
writer: new StringWriter());
}
private class TestRazorPage : RazorPage<RazorPageCreateModelExpressionModel>
{
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
public class RazorPageCreateModelExpressionModel
{
public int Id { get; set; }
public string Name { get; set; }
public RazorPageCreateModelExpressionSubModel SubModel { get; set; }
}
public class RazorPageCreateModelExpressionSubModel
{
public int Id { get; set; }
public string Name { get; set; }
public RazorPageCreateModelExpressionSubSubModel SubSubModel { get; set; }
}
public class RazorPageCreateModelExpressionSubSubModel
{
public int Id { get; set; }
public string Name { get; set; }
}
}
}