Per AJAX serverseitig Partial Views rendern und diese dann dynamisch ins HTML-DOM einfügen

Nachfolgendes Code-Fragment hat sich bei uns seit Jahren bewährt, in Projekten mit ASP.NET Core, wenn es darum geht, per AJAX serverseitig eine View zu rendern und das Ergebnis zurück zu geben.

ViewRenderService.cs:

using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System.Text;

public interface IViewRenderService
{
    /// <summary>
    /// <para>Ein Aufruf dieser Methode ist immer dann sinnvoll, wenn neben dem eigentlichen HTML eines Partials noch
    /// JSON aus einer Action zurück gegeben werden soll. Der Rückgabewert dieses Methodenaufrufs kann dann z. B.
    /// als eine "html"-Property im JSON zurück gegeben werden.</para>
    /// 
    /// <para>Wenn ausschließlich das HTML eines Partials zurück gegeben werden soll, also kein zusätzliches JSON,
    /// dann reicht ein "return PartialView(...)" aus der Action heraus aus, und diese Methode wird nicht benötigt.</para>
    /// </summary>
    Task<string> RenderPartialView(
        ControllerBase controller,
        [AspMvcPartialView] string viewPath,
        object model,
        ViewDataDictionary? viewData = null);
}

public sealed class ViewRenderService : IViewRenderService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly HttpContext? _context;

    public ViewRenderService(
        IRazorViewEngine razorViewEngine,
        ITempDataProvider tempDataProvider,
        IHttpContextAccessor accessor)
    {
        _razorViewEngine = razorViewEngine;
        _tempDataProvider = tempDataProvider;
        _context = accessor.HttpContext;
    }

    /// <inheritdoc cref="IViewRenderService.RenderPartialView"/>
    public async Task<string> RenderPartialView(
        ControllerBase controller,
        [AspMvcPartialView] string viewPath,
        object model,
        ViewDataDictionary? viewData = null)
    {
        var actionContext = controller.ControllerContext;

        await using var sw = new StringWriter();
        var viewResult = _razorViewEngine.FindView(actionContext, viewPath, false);
        if (!viewResult.Success)
        {
            var msg = new StringBuilder();
            msg.AppendLine($@"{viewPath} does not match any available view.");
            msg.AppendLine(@"SearchedLocations:");
            msg.AppendLine(string.Join($@"{Environment.NewLine}", viewResult.SearchedLocations));

            throw new Exception(msg.ToString());
        }

        var viewDictionary =
            viewData ?? new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
        viewDictionary.Model = model;

        var viewContext =
            new ViewContext(actionContext, viewResult.View, viewDictionary,
                new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                sw, new HtmlHelperOptions())
            {
                RouteData = _context?.GetRouteData() ?? new RouteData()
            };

        await viewResult.View.RenderAsync(viewContext);
        return sw.ToString();
    }
}

Das funktioniert sowohl mit jQuery-AJAX-Aufrufen, als auch mit ASP.NET-Core-Formularen, die per jQuery Unobtrusive Ajax submitted werden.

Es gibt noch eine JavaScript-Funktion renderFormGroupsInContainer, die nach einem DOM-Einfügen eines erhaltenen HTML-Fragments aufgerufen werden sollte, siehe auch nachfolgende Beispiele:

class App {
    /**
     * Diese Funktion sollte nach jedem Ajax-GET-Callback ausgeführt werden.
     * u. a. wird die Client-Validierung aktiviert, sowie die Inline-Labels.
     */
    static renderFormGroupsInContainer($container: any) {

        // Code hier aus "km.forms.js", Funktion "renderFormGroupsInContainer" übernommen.
        // TODO: Später ggf. noch mehr Code von dort übernehmen.

        //Client-Validation
        if ($.validator !== undefined && $.validator.unobtrusive !== undefined) {
            $.validator.unobtrusive.parse($container);
        }

        //Bootstrap - Popover initialisieren
        $("[data-toggle='popover']").popover();
    }
}

Beispiele

Auf GitHub habe ich ein Beispiel-Projekt veröffentlicht:

Dieses Beispiel u. a. auch:

  • Verschachtelte Aufrufe (gerendertes Partial, das via AJAX geliefert wurde ruft weiteres Partial via AJAX ab).
  • JavaScript-Funktionen werden auch bei via AJAX gelieferten Partials korrekt gebunden.
  • Aufruf von „globalen“ JavaScript-Funktionen aus dem JavaScript in den gelieferten Partials funktioniert.