Merge pull request #7680 from aspnet/release/2.1
Allow PartialTagHelper to specify a null model. Fixes #7667
This commit is contained in:
commit
1a5c9e548f
|
|
@ -22,6 +22,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
{
|
{
|
||||||
private const string ForAttributeName = "for";
|
private const string ForAttributeName = "for";
|
||||||
private const string ModelAttributeName = "model";
|
private const string ModelAttributeName = "model";
|
||||||
|
private object _model;
|
||||||
|
private bool _hasModel;
|
||||||
|
private bool _hasFor;
|
||||||
|
private ModelExpression _for;
|
||||||
|
|
||||||
private readonly ICompositeViewEngine _viewEngine;
|
private readonly ICompositeViewEngine _viewEngine;
|
||||||
private readonly IViewBufferScope _viewBufferScope;
|
private readonly IViewBufferScope _viewBufferScope;
|
||||||
|
|
@ -43,13 +47,29 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
/// An expression to be evaluated against the current model. Cannot be used together with <see cref="Model"/>.
|
/// An expression to be evaluated against the current model. Cannot be used together with <see cref="Model"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HtmlAttributeName(ForAttributeName)]
|
[HtmlAttributeName(ForAttributeName)]
|
||||||
public ModelExpression For { get; set; }
|
public ModelExpression For
|
||||||
|
{
|
||||||
|
get => _for;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_for = value ?? throw new ArgumentNullException(nameof(value));
|
||||||
|
_hasFor = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The model to pass into the partial view. Cannot be used together with <see cref="For"/>.
|
/// The model to pass into the partial view. Cannot be used together with <see cref="For"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HtmlAttributeName(ModelAttributeName)]
|
[HtmlAttributeName(ModelAttributeName)]
|
||||||
public object Model { get; set; }
|
public object Model
|
||||||
|
{
|
||||||
|
get => _model;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_model = value;
|
||||||
|
_hasModel = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
|
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
|
||||||
|
|
@ -88,7 +108,12 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
// Internal for testing
|
// Internal for testing
|
||||||
internal object ResolveModel()
|
internal object ResolveModel()
|
||||||
{
|
{
|
||||||
if (Model != null & For != null)
|
// 1. Disallow specifying values for both Model and For
|
||||||
|
// 2. If a Model was assigned, use it even if it's null.
|
||||||
|
// 3. For cannot have a null value. Use it if it was assigned to.
|
||||||
|
// 4. Fall back to using the Model property on ViewContext.ViewData if none of the above conditions are met.
|
||||||
|
|
||||||
|
if (_hasFor && _hasModel)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
Resources.FormatPartialTagHelper_InvalidModelAttributes(
|
Resources.FormatPartialTagHelper_InvalidModelAttributes(
|
||||||
|
|
@ -97,17 +122,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
ModelAttributeName));
|
ModelAttributeName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Model != null)
|
if (_hasModel)
|
||||||
{
|
{
|
||||||
return Model;
|
return Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (For != null)
|
if (_hasFor)
|
||||||
{
|
{
|
||||||
return For.Model;
|
return For.Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model and For are null, fallback to the ViewContext's ViewData model.
|
// A value for Model or For was not specified, fallback to the ViewContext's ViewData model.
|
||||||
return ViewContext.ViewData.Model;
|
return ViewContext.ViewData.Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ using System.Net.Http.Headers;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using AngleSharp.Dom.Html;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
@ -554,6 +556,46 @@ Products: Music Systems, Televisions (3)";
|
||||||
Assert.Contains("Hello, Private World!", response);
|
Assert.Contains("Hello, Private World!", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PartialTagHelper_AllowsPassingModelValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var url = "/HtmlGeneration_Home/StatusMessage";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var document = await Client.GetHtmlDocumentAsync(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var banner = QuerySelector(document, ".banner");
|
||||||
|
Assert.Equal("Some status message", banner.TextContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PartialTagHelper_AllowsPassingNullModelValue()
|
||||||
|
{
|
||||||
|
// Regression test for https://github.com/aspnet/Mvc/issues/7667.
|
||||||
|
// Arrange
|
||||||
|
var url = "/HtmlGeneration_Home/NullStatusMessage";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var document = await Client.GetHtmlDocumentAsync(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var banner = QuerySelector(document, ".banner");
|
||||||
|
Assert.Empty(banner.TextContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IElement QuerySelector(IHtmlDocument document, string selector)
|
||||||
|
{
|
||||||
|
var element = document.QuerySelector(selector);
|
||||||
|
if (element == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Document does not contain element that matches the selector {selector}: " + Environment.NewLine + document.DocumentElement.OuterHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
private static HttpRequestMessage RequestWithLocale(string url, string locale)
|
private static HttpRequestMessage RequestWithLocale(string url, string locale)
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,28 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
Assert.Same(expectedModel, model);
|
Assert.Same(expectedModel, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveModel_ReturnsModelWhenNullValueIsProvided()
|
||||||
|
{
|
||||||
|
// Regression test for https://github.com/aspnet/Mvc/issues/7667.
|
||||||
|
// Arrange
|
||||||
|
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
|
||||||
|
{
|
||||||
|
Model = new object(),
|
||||||
|
};
|
||||||
|
var tagHelper = new PartialTagHelper(Mock.Of<ICompositeViewEngine>(), Mock.Of<IViewBufferScope>())
|
||||||
|
{
|
||||||
|
Model = null,
|
||||||
|
ViewData = viewData,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var model = tagHelper.ResolveModel();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(model);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveModel_ReturnsForModelWhenProvided()
|
public void ResolveModel_ReturnsForModelWhenProvided()
|
||||||
{
|
{
|
||||||
|
|
@ -67,7 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveModel_ReturnsViewContextsViewDataModelWhenModelAndForAreNull()
|
public void ResolveModel_ReturnsViewContextsViewDataModelWhenModelAndForAreNotSet()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expectedModel = new object();
|
var expectedModel = new object();
|
||||||
|
|
@ -107,6 +129,28 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
Assert.Equal(expectedMessage, exception.Message);
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveModel_ThrowsWhenNullModelAndForProvided()
|
||||||
|
{
|
||||||
|
// 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 = null,
|
||||||
|
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]
|
[Fact]
|
||||||
public async Task ProcessAsync_RendersPartialView_IfGetViewReturnsView()
|
public async Task ProcessAsync_RendersPartialView_IfGetViewReturnsView()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -239,5 +239,9 @@ namespace HtmlGenerationWebSite.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult PartialTagHelperWithoutModel() => View();
|
public IActionResult PartialTagHelperWithoutModel() => View();
|
||||||
|
|
||||||
|
public IActionResult StatusMessage() => View(new StatusMessageModel { Message = "Some status message"});
|
||||||
|
|
||||||
|
public IActionResult NullStatusMessage() => View("StatusMessage", new StatusMessageModel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace HtmlGenerationWebSite.Models
|
||||||
|
{
|
||||||
|
public class StatusMessageModel
|
||||||
|
{
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
@using HtmlGenerationWebSite.Models
|
||||||
|
@model StatusMessageModel
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|
||||||
|
<partial name="_StatusMessagePartial" model="Model.Message" />
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
@model string
|
||||||
|
<div class="banner">@Model</div>
|
||||||
Loading…
Reference in New Issue