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:
parent
1ff5bdca79
commit
e94d77c47f
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue