Add model attribute for PartialTagHelper.

- The model attribute is used to define any object based model to be passed to a `TagHelper`. It enables scenarios when users want to pass in `new` poco types.
- Added unit tests for the new `ResolveModel` method in `PartialTagHelper`.
- Added a single functional test to verify the end-to-end.

#7374
This commit is contained in:
N. Taylor Mullen 2018-03-23 11:10:43 -07:00
parent 1ff5bdca79
commit e94d77c47f
9 changed files with 195 additions and 6 deletions

View File

@ -20,6 +20,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlTargetElement("partial", Attributes = "name", TagStructure = TagStructure.WithoutEndTag)]
public class PartialTagHelper : TagHelper
{
private const string ForAttributeName = "for";
private const string ModelAttributeName = "model";
private readonly ICompositeViewEngine _viewEngine;
private readonly IViewBufferScope _viewBufferScope;
@ -37,10 +40,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
public string Name { get; set; }
/// <summary>
/// An expression to be evaluated against the current model.
/// An expression to be evaluated against the current model. Cannot be used together with <see cref="Model"/>.
/// </summary>
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
/// <summary>
/// The model to pass into the partial view. Cannot be used together with <see cref="For"/>.
/// </summary>
[HtmlAttributeName(ModelAttributeName)]
public object Model { get; set; }
/// <summary>
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
/// </summary>
@ -63,10 +73,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(context));
}
var model = ResolveModel();
var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize);
using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
{
await RenderPartialViewAsync(writer);
await RenderPartialViewAsync(writer, model);
// Reset the TagName. We don't want `partial` to render.
output.TagName = null;
@ -74,7 +85,33 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
}
private async Task RenderPartialViewAsync(TextWriter writer)
// Internal for testing
internal object ResolveModel()
{
if (Model != null & For != null)
{
throw new InvalidOperationException(
Resources.FormatPartialTagHelper_InvalidModelAttributes(
typeof(PartialTagHelper).FullName,
ForAttributeName,
ModelAttributeName));
}
if (Model != null)
{
return Model;
}
if (For != null)
{
return For.Model;
}
// Model and For are null, fallback to the ViewContext's ViewData model.
return ViewContext.ViewData.Model;
}
private async Task RenderPartialViewAsync(TextWriter writer, object model)
{
var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false);
var getViewLocations = viewEngineResult.SearchedLocations;
@ -99,9 +136,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var view = viewEngineResult.View;
// Determine which ViewData we should use to construct a new ViewData
var baseViewData = ViewData ?? ViewContext.ViewData;
// Use the rendering View's model only if an for expression does not exist
var model = For != null ? For.Model : ViewContext.ViewData.Model;
var newViewData = new ViewDataDictionary<object>(baseViewData, model);
var partialViewContext = new ViewContext(ViewContext, view, newViewData, writer);

View File

@ -192,6 +192,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
internal static string FormatViewEngine_PartialViewNotFound(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("ViewEngine_PartialViewNotFound"), p0, p1);
/// <summary>
/// Cannot use '{0}' with both '{1}' and '{2}' attributes.
/// </summary>
internal static string PartialTagHelper_InvalidModelAttributes
{
get => GetString("PartialTagHelper_InvalidModelAttributes");
}
/// <summary>
/// Cannot use '{0}' with both '{1}' and '{2}' attributes.
/// </summary>
internal static string FormatPartialTagHelper_InvalidModelAttributes(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("PartialTagHelper_InvalidModelAttributes"), p0, p1, p2);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -156,4 +156,7 @@
<data name="ViewEngine_PartialViewNotFound" xml:space="preserve">
<value>The partial view '{0}' was not found. The following locations were searched:{1}</value>
</data>
<data name="PartialTagHelper_InvalidModelAttributes" xml:space="preserve">
<value>Cannot use '{0}' with both '{1}' and '{2}' attributes.</value>
</data>
</root>

View File

@ -62,5 +62,13 @@ Product_2 description</textarea>
<div>HtmlFieldPrefix = </div>
<input type="submit" />
<input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
<hr />
<h2>You might also like these products!</h2>
<ul>
<li><a href="http://www.contoso.com/">THE Best Product</a></li>
</ul>
</body>
</html>

View File

@ -26,5 +26,13 @@
<div>HtmlFieldPrefix = </div>
<input type="submit" />
<input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
<hr />
<h2>You might also like these products!</h2>
<ul>
<li><a href="http://www.contoso.com/">THE Best Product</a></li>
</ul>
</body>
</html>

View File

@ -23,6 +23,90 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class PartialTagHelperTest
{
[Fact]
public void ResolveModel_ReturnsModelWhenProvided()
{
// Arrange
var expectedModel = new object();
var tagHelper = new PartialTagHelper(Mock.Of<ICompositeViewEngine>(), Mock.Of<IViewBufferScope>())
{
Model = expectedModel,
};
// Act
var model = tagHelper.ResolveModel();
// Assert
Assert.Same(expectedModel, model);
}
[Fact]
public void ResolveModel_ReturnsForModelWhenProvided()
{
// Arrange
var expectedModel = new PropertyModel();
var modelMetadataProvider = new TestModelMetadataProvider();
var containerModel = new TestModel()
{
Property = expectedModel
};
var containerModelExplorer = modelMetadataProvider.GetModelExplorerForType(
typeof(TestModel),
containerModel);
var propertyModelExplorer = containerModelExplorer.GetExplorerForProperty(nameof(TestModel.Property));
var tagHelper = new PartialTagHelper(Mock.Of<ICompositeViewEngine>(), Mock.Of<IViewBufferScope>())
{
For = new ModelExpression("Property", propertyModelExplorer),
};
// Act
var model = tagHelper.ResolveModel();
// Assert
Assert.Same(expectedModel, model);
}
[Fact]
public void ResolveModel_ReturnsViewContextsViewDataModelWhenModelAndForAreNull()
{
// Arrange
var expectedModel = new object();
var viewContext = GetViewContext();
viewContext.ViewData.Model = expectedModel;
var tagHelper = new PartialTagHelper(Mock.Of<ICompositeViewEngine>(), Mock.Of<IViewBufferScope>())
{
ViewContext = viewContext
};
// Act
var model = tagHelper.ResolveModel();
// Assert
Assert.Same(expectedModel, model);
}
[Fact]
public void ResolveModel_ThrowsWhenModelAndForProvided()
{
// Arrange
var modelMetadataProvider = new TestModelMetadataProvider();
var containerModel = new TestModel();
var containerModelExplorer = modelMetadataProvider.GetModelExplorerForType(
typeof(TestModel),
containerModel);
var propertyModelExplorer = containerModelExplorer.GetExplorerForProperty(nameof(TestModel.Property));
var tagHelper = new PartialTagHelper(Mock.Of<ICompositeViewEngine>(), Mock.Of<IViewBufferScope>())
{
Model = new object(),
For = new ModelExpression("Property", propertyModelExplorer),
};
var expectedMessage = Resources.FormatPartialTagHelper_InvalidModelAttributes(typeof(PartialTagHelper).FullName, "for", "model");
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => tagHelper.ResolveModel());
Assert.Equal(expectedMessage, exception.Message);
}
[Fact]
public async Task ProcessAsync_RendersPartialView_IfGetViewReturnsView()
{

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. 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.Collections.Generic;
namespace HtmlGenerationWebSite.Models
{
public class ProductRecommendations
{
public ProductRecommendations(params Product[] products)
{
if (products == null)
{
throw new ArgumentNullException(nameof(products));
}
Products = products;
}
public IEnumerable<Product> Products { get; }
}
}

View File

@ -21,5 +21,7 @@
<div>HtmlFieldPrefix = @ViewData.TemplateInfo.HtmlFieldPrefix</div>
<input type="submit" />
</form>
<partial name="_ProductRecommendations" model='new ProductRecommendations(new Product() { ProductName = "THE Best Product", HomePage = new Uri("http://www.contoso.com")})' />
</body>
</html>

View File

@ -0,0 +1,13 @@
@using HtmlGenerationWebSite.Models
@model ProductRecommendations
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<hr />
<h2>You might also like these products!</h2>
<ul>
@foreach (var product in Model.Products)
{
<li><a href="@product.HomePage">@product.ProductName</a></li>
}
</ul>