Reputation: 2089
I'm using the example found here to allow the rendering of a partial view from within a tag helper. What I'm going for here is to be able to define a tag helper like so:
<mydateinput for="@Model.StartDate" />
Inside the c# code of the tag helper, I'd define the "for" property, and from what I can tell this needs to be defined as a "ModelExpression".
public class MyDateInputTagHelper : TagHelper
{
public ModelExpression For { get; set; }
...
}
Using the code mentioned earlier in this article, I am rendering a partial view, and simply passing the class of the tag helper as the model for the partial view.
public override void Process(TagHelperContext context, TagHelperOutput output)
{
base.Process(context, output);
((IViewContextAware)HtmlHelper).Contextualize(ViewContext);
output.Content.SetHtmlContent(HtmlHelper.Partial("~/Views/Partials/TagHelpers/MyDateInput.cshtml", this));
}
Lastly, my partial view is defined something like this
<input asp-for="For" />
The problem I'm running into is that I can't get the model expression "For" to pass into the partial view correctly. When I view the html source, I just see literally the name "For" in the id & name attributes of the input. Also, the value set in my model of the razor page isn't correctly showing either.
What I want to happen is the html would be rendered in such a way that upon posting the page, the model to my razor page would be populated with the values that have been selected underneath the tag helper / partial view. It would specifically be around "StartDate" (in my example), not the property "For".
Does anyone have an idea of what I'm doing wrong, and what I can change in this example to correctly pass the ModelExpression through to a partial view?
Upvotes: 5
Views: 4103
Reputation: 9656
Alternatively use Html Helpers in your partial and then it will work.
// _MovieField.cshtml
@{
string propertyName = (string)ViewData["PropertyName"];
}
<div class="form-group">
@Html.Label(propertyName, null, new{ @class = "control-label" })
@Html.Editor(propertyName, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessage(propertyName, null, new{ @class = "text-danger" })
</div>
And use it like this
// EditMovie.cshtml
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Movie.Id" />
<partial name="_MovieField" for="Movie"
view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "Title" }}' />
<partial name="_MovieField" for="Movie"
view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "ReleaseDate" }}' />
<partial name="_MovieField" for="Movie"
view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "Genre" }}' />
<partial name="_MovieField" for="Movie"
view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "Price" }}' />
<partial name="_MovieField" for="Movie"
view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "Rating" }}' />
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
Upvotes: 0
Reputation: 25360
It's not possible to do that with the InputTagHelper
directly. For more information, see Razor/issues #926 and also a similar question on SO.
A Workaround
But as a workaround, you could use a custom HtmlHelper<TModel>
(just like @Html
) to achieve the same goal. Your partial view might look like this:
@model App.TagHelpers.PartialVM
@{
var PartialHtml = Model.HtmlHelper;
}
@PartialHtml.Label(Model.NewFor.Name,Model.NewFor.Name)
@PartialHtml.TextBox(Model.NewFor.Name, Model.NewModel)
Namely, use @Html.Label()
& @Html.TextBot
rather than <label>
& <input>
.
Here the PartialVM
is a simple class that holds the meta info about the model :
public class PartialVM
{
public PartialVM(ModelExpression originalFor, IHtmlHelper htmlHelper)
{
var originalExplorer = originalFor.ModelExplorer;
OriginalFor = originalFor;
OriginalExplorer = originalExplorer;
NewModel = originalExplorer.Model;
NewModelExplorer = originalExplorer.GetExplorerForModel(NewModel);
NewFor = new ModelExpression(OriginalFor.Name, NewModelExplorer);
this.HtmlHelper = htmlHelper;
}
public IHtmlHelper HtmlHelper { get; set; }
public ModelExpression OriginalFor { get; set; }
public ModelExplorer OriginalExplorer { get; set; }
public ModelExpression NewFor { get; set; }
public ModelExplorer NewModelExplorer { get; set; }
public Object NewModel { get; set; }
}
Note the IHtmlHelper
is actually an IHtmlHelper<TSomeDynamicModel>
instead of a plain IHtmlHelper
.
Lastly, change your TagHelper
as below:
[HtmlTargetElement("my-custom-input")]
public class MyCustomInputTagHelper : TagHelper
{
private readonly IServiceProvider _sp;
[ViewContext]
public ViewContext ViewContext { set; get; }
public ModelExpression For { get; set; }
public MyCustomInputTagHelper(IServiceProvider sp)
{
this._sp = sp;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
base.Process(context, output);
var originExplorer = For.ModelExplorer;
var newModel = originExplorer.Model;
var newExplorer = originExplorer.GetExplorerForModel(newModel);
var newFor = new ModelExpression(For.Name, newExplorer);
var ModelType = originExplorer.Container.Model.GetType();
var htmlHelperType = typeof(IHtmlHelper<>).MakeGenericType(ModelType);
var htmlHelper = this._sp.GetService(htmlHelperType) as IHtmlHelper; // get the actual IHtmlHelper<TModel>
(htmlHelper as IViewContextAware).Contextualize(ViewContext);
var vm = new PartialVM(For, htmlHelper);
var writer = new StringWriter();
var content = await htmlHelper.PartialAsync("~/Views/Partials/TagHelpers/MyDateInput.cshtml", vm);
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
output.Content.SetHtmlContent(content);
}
}
Now you could pass any asp-for
expression string for the For
property of your TagHelper
, and it should work as expected.
Test Case:
Suppose we have a Dto model :
public class XModel {
public int Id { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string ActiveStatus{ get; set; }
}
You could render it in the following way :
/// the action method looks like:
/// var model = new XModel {
/// StartDate = DateTime.Now,
/// EndDate = DateTime.Now.AddYears(1),
/// ActiveStatus = "Active",
/// };
/// return View(model);
@model XModel
<my-custom-input For="StartDate" />
<my-custom-input For="EndDate" />
<my-custom-input For="ActiveStatus" />
Here's a screenshot when it renders:
Upvotes: 6