raterus
raterus

Reputation: 2089

Pass ModelExpression from a Tag Helper to a Partial View

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

Answers (2)

Neutrino
Neutrino

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

itminus
itminus

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:

enter image description here

Upvotes: 6

Related Questions