Widgets

Add stats, lists and other blocks above, beside or below a view page's fields

You can add widgets to a resource's view page, such as a list of the traveller's other bookings beside a booking's fields. View page widgets work like home page widgets: each one is rendered using a partial view that you write, and can have a handler that loads its data.

A booking's view page with the traveller's other bookings beside the fields and the booking's notes below them

Adding a widget

To add widgets to the view page, call Widgets() in the AllowView() callback, and add each widget with Add(). The example below adds a widget below a booking's fields that renders the _BookingNotesWidget partial view:

resource.AllowView(view =>
{
    view.Fields(fields =>
    {
        fields.Add(booking => booking.Reference);
        fields.Add(booking => booking.Traveller);
    });
    view.Widgets(widgets =>
    {
        widgets.Add("Notes", "_BookingNotesWidget", widget =>
        {
            widget.Placement = WidgetPlacement.Bottom;
        });
    });
});

Placement sets where the widget is displayed: WidgetPlacement.Top above the fields, which is the default, WidgetPlacement.Aside in a column beside them, or WidgetPlacement.Bottom below them. When the page is narrow, the column is displayed below the fields.

Handlers, parameters, controls and the other widget settings work as they do on the home page. See home page widgets for how to write them.

Put the partial view in Areas/StellarAdmin/Views/{slug}/, such as Areas/StellarAdmin/Views/bookings/. To use a partial view on several pages, put it in Areas/StellarAdmin/Views/Shared/.

Using the record

The functions you pass to Parameters() and ShowWhen() receive a RecordWidgetContext<T>, where T is the page's model. Besides the HttpContext, User and Services of a WidgetContext, it has the following properties:

PropertyDescription
KeyThe key of the record the page displays, as it appears in the page's URL.
RecordThe record the page displays. If the view page has its own model, this is that model.

A widget without a handler receives the RecordWidgetContext<T> as its partial view's model, so it can display the record without a handler. The example below is the _BookingNotesWidget partial view from the first example:

@model RecordWidgetContext<Booking>
<sa-card>
    <sa-card-header>
        <sa-card-title>Notes</sa-card-title>
    </sa-card-header>
    <sa-card-content>
        <p>@(Model.Record.SpecialRequests ?? "No special requests")</p>
    </sa-card-content>
</sa-card>

A handler only receives what Parameters() returns, which StellarAdmin puts in the widget's URL. Pass the record's key rather than the record, and load what the widget needs in the handler. The example below lists the traveller's other bookings beside the booking's fields:

resource.AllowView(view =>
{
    view.Widgets(widgets =>
    {
        widgets.Add<OtherBookingsHandler>("Other bookings", "_OtherBookingsWidget", widget =>
        {
            widget.Placement = WidgetPlacement.Aside;
            widget.Parameters(context => new OtherBookingsParameters(context.Record.Id));
        });
    });
});

The handler receives the booking's Id, and loads the other bookings of the booking's traveller:

using StellarAdmin.Widgets;

public record OtherBookingsParameters(int BookingId);

public record OtherBooking(string Reference, string Trip);

public class OtherBookingsHandler(ApplicationDbContext db)
    : IWidgetHandler<OtherBookingsParameters, IReadOnlyList<OtherBooking>>
{
    public async Task<IReadOnlyList<OtherBooking>?> LoadAsync(
        OtherBookingsParameters parameters,
        WidgetLoadContext context,
        CancellationToken cancellationToken)
    {
        var travellerId = await db.Bookings
            .Where(booking => booking.Id == parameters.BookingId)
            .Select(booking => booking.TravellerId)
            .SingleAsync(cancellationToken);

        return await db.Bookings
            .Where(booking => booking.TravellerId == travellerId && booking.Id != parameters.BookingId)
            .OrderBy(booking => booking.Reference)
            .Select(booking => new OtherBooking(booking.Reference, booking.Departure.Trip.Title))
            .ToListAsync(cancellationToken);
    }
}

On this page