All articles

Render a Partial View with a Model in ASP.NET Core MVC

Pass models into partials from views and controllers, return PartialViewResult for AJAX fragment updates, and avoid the HtmlFieldPrefix binding trap in forms.

0 · log in to like, save & follow Share on LinkedIn Share on X
Render a Partial View with a Model in ASP.NET Core MVC

The previous post in this pair covered what partial views are; this one goes deep on the part that trips people up in practice: getting the right model into the partial — from a parent view, from a controller action via PartialViewResult, and over AJAX so a fragment of the page refreshes without a full reload. All examples target .NET 10 and are verified against the companion repository.

Render a Partial View with a Model in ASP.NET Core MVC

The strongly-typed contract

A partial declares its model like any view, and that declaration is a contract the compiler enforces:

@* Views/Shared/_ProductCard.cshtml *@
@model AspNetCoreMvcExamples.Models.Product

<div class="card mb-3">
    <div class="card-body">
        <h5 class="card-title">@Model.Name</h5>
        <p class="card-text">₹@Model.Price.ToString("N2")</p>
    </div>
</div>

Pass the wrong type and you get a clear runtime error naming the expected and actual model types — far better than the null surprises of ViewBag-based data passing.

Passing a model from a parent view

The model attribute of the <partial> tag helper accepts any expression:

@model IReadOnlyList<AspNetCoreMvcExamples.Models.Product>

@foreach (var product in Model)
{
    <partial name="_ProductCard" model="product" />
}

@* an element of the model *@
<partial name="_ProductCard" model="Model[0]" />

@* a property path *@
<partial name="_OrderSummary" model="Model.Order.Summary" />

Three behaviors to internalize:

  • Omit model and the partial receives the parent's model. Handy for splitting one big page into _Header, _Pricing, _Reviews sections that all share the page model.
  • Model is null-checked at render time — if the expression evaluates to null and the partial dereferences it, you get the NullReferenceException inside the partial. Guard in the parent (@if (Model.Order is not null)) rather than littering the partial with null checks.
  • Each render is independent. The loop above renders the same file three times with three different models; partials hold no state between renders.

Returning a partial from a controller

A controller action can return just the fragment with PartialView, which produces a PartialViewResult — same view resolution, no layout:

public class ProductsController : Controller
{
    // GET /Products/Card/2 — returns only the card's HTML
    public IActionResult Card(int id)
    {
        var product = Catalog.Products.FirstOrDefault(p => p.Id == id);
        return product is null ? NotFound() : PartialView("_ProductCard", product);
    }
}

The response is the card's markup and nothing else — no <html>, no layout chrome:

<div class="card mb-3">
    <div class="card-body">
        <h5 class="card-title">4K monitor</h5>
        <p class="card-text">₹329.99</p>
        ...
    </div>
</div>

This is the piece that makes partials more than a DRY device: they become the server-rendered unit of page updates.

Refreshing a fragment over AJAX

With an endpoint that returns a partial, updating part of the page is a fetch plus an innerHTML swap:

<div id="featured"></div>

<script>
    async function showProduct(id) {
        const res = await fetch(`/Products/Card/${id}`);
        document.getElementById('featured').innerHTML = await res.text();
    }
</script>

The server keeps owning the markup — one Razor file renders the card whether it appears in the initial page or arrives later via fetch. Compared to returning JSON and templating on the client, you trade some payload size for a single source of truth and zero client-side templates. For list filtering, paging, and "load more" buttons, this pattern is hard to beat in an MVC app. (If you find yourself building many such endpoints, that's the niche libraries like htmx formalize.)

Models in form partials: the naming trap

When a partial renders inputs, the model expression affects the generated name attributes, and names are what model binding reads on POST. Rendering a child object's editor this way:

<partial name="_AddressEditor" model="Model.ShippingAddress" />

produces inputs named Street, City — but the POST action expects ShippingAddress.Street. The binder finds nothing and your address comes back empty. The fix is to pass the field prefix through ViewData.TemplateInfo:

<partial name="_AddressEditor" model="Model.ShippingAddress"
         view-data='new ViewDataDictionary(ViewData) { TemplateInfo = { HtmlFieldPrefix = "ShippingAddress" } }' />

Now asp-for="Street" inside the partial renders name="ShippingAddress.Street" and binding works. For collections, the prefix must include the index (Items[0]), which is why editor templates or explicit for loops in the parent are often cleaner than partials for collection editing.

ViewData flows down, not up

A partial receives a copy of the parent's ViewData (plus whatever you add via the view-data attribute). It can read ViewData["Title"], but writes do not propagate back to the parent — partials cannot set the page title or inject into layout sections. @section inside a partial is silently ignored; script that a partial needs must be registered by the page that hosts it.

Choosing the mechanism

  • Parent already has the data → <partial model="..." />.
  • Fragment needs its own data on initial render → view component.
  • Fragment must refresh after load → controller action returning PartialView + fetch.
  • Editing nested objects/collections → mind HtmlFieldPrefix, or use editor templates.

Summary

A partial's @model is a compile-checked contract; fill it from the parent with the model attribute, or from a controller with PartialView(name, model) and swap the HTML over fetch for lightweight page updates. When partials render form fields, set HtmlFieldPrefix so posted names match what the binder expects. The product-card endpoint and the AJAX swap are both runnable from the companion repository.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.