# Example: .NET Framework 4 MVC screen document

Doc gen produces one screen document per non-utility screen in a .NET Framework
4 ASP.NET MVC 5 front end that uses Razor views, written to
`docs/screens/<slug>.md` from the controller action, its Razor view and its
ViewModel. This is a different document from the
[ASP.NET Core Razor Pages screen](ncore-screen.md) and the
[BOF WebForms screen](frontend-screen.md): a screen is a compiled controller
action that returns a full view, and its interface is actions and routes rather
than page handlers or `.aspx` controls.

## What reviewers should check

- **Screens are compiled actions that return a full view.** One document per
  `View()` action with a layout, not one per controller. Partial views, dialogs,
  grid, JSON and remote-validator actions are not screens, and
  `docs/screens/index.md` is reconciled against the compiled actions.
- **The Route table uses the real route table.** The URL is what `RouteConfig`
  produces for the controller and action, not an invented path, with one row per
  verb when GET and POST share an action name.
- **Operations use the MVC request form.** The `Request` column is
  `VERB Controller/Action`, without the `Controller` suffix. A `?handler=`
  request or a `[SecurityApiAuthorize]` role means the Razor Pages vocabulary
  leaked in, and a postback, `ViewState` or `.aspx` reference means the
  WebForms vocabulary did; both are defects.
- **Access is the effective `[SaifAuthorize]` requirement.** The roles come from
  the action attribute, otherwise the controller's, otherwise the local
  `BaseController`'s, and each row names where it was declared. In-action checks
  such as `CanUserUpdate()` that change what the user sees are rows too.
- **Delegation is sourced.** Every `Delegates to` value is a resolvable link to
  a library operation or application workflow, a `†`-marked dotted library
  name, or `—` for screen-local actions. The delegated operation's steps are
  not restated.
- **Wiring stays off the screen page.** The Autofac composition root lives in
  the application companion project (here `SAIF.DependencyMaps.Web.PolicyRenewals`)
  and is documented on `docs/application/architecture.md`, not on screens.
- **AJAX rows link to `hosted-endpoints.md`.** Each row in
  `## AJAX and grid actions` links its entry on
  `docs/application/hosted-endpoints.md`; partial views belong in
  `## Dialogs and partials`.
- **Sources are controller, view and ViewModel files.** An `.aspx` file or a
  code-behind file as a source is a blocker.
- **Rule IDs are links** to their anchor on
  `docs/application/business-rules.md`.
- A `## Screen flow diagram` appears only when the screen has three or more
  outbound navigations. A cross-application target names the configuration key
  that holds its URL.

## Example

The document below is a complete, correct `renewal-quote.md` for a fictional
application.

[View rendered example](rendered/screens/renewal-quote.md) — the same document as a real page, so you can compare rendering as well as source.

````markdown
---
title: Renewal Quote
summary: Lets underwriting staff review a policy's renewal premium by class code, adjust the quote lines, and issue the renewal offer to the employer.
status: draft
owner: Policy Renewals Team
last_reviewed: 2026-09-09
---

# Renewal Quote

Renewal Quote is the main working screen of the Policy Renewals application.
Underwriting staff open it from the Renewal Queue for a policy that is
approaching expiration and use it to review the quote the rating library has
produced: the premium for each class code, the experience modifier, and the
renewal effective date. The user can adjust individual premium lines, give a
reason for the adjustment, save the quote, and issue the renewal offer to the
employer. The action loads the quote for the `policyNumber` route value and
returns a state view when none can be produced. From here the user returns to
the queue, opens the quote history, or moves to the policy summary in the
Policy Services Portal.

## Route

| Controller | Action | Verb | URL | Parameters | View | Layout |
| --- | --- | --- | --- | --- | --- | --- |
| `RenewalController` | `Quote` | GET | `/Renewal/Quote/{policyNumber}` | `policyNumber` | `Views/Renewal/Quote.cshtml` | `_Layout` |
| `RenewalController` | `Quote` | POST | `/Renewal/Quote/{policyNumber}` | `policyNumber`, `RenewalQuoteViewModel` | `Views/Renewal/Quote.cshtml` (redisplayed when validation fails) | `_Layout` |

## Access

| Requirement | Declared on | Effect |
| --- | --- | --- |
| `[SaifAuthorize(Roles = "Renewals.Read,Renewals.Underwriting")]` | `BaseController` | Only users holding one of the two roles can open any action of a controller that derives from `BaseController` |
| `[SaifAuthorize(Roles = "Renewals.Underwriting")]` | `RenewalController.IssueOffer` | Only underwriting staff can issue a renewal offer |
| `CanUserUpdate()` | `RenewalController.Quote` | Returns `true` only for users in `Renewals.Underwriting`; when `false`, `IsReadOnly` is set, the grid shows no edit commands, and **Save quote** and **Issue offer** are hidden |

## Entry conditions
- The user selects a policy in the Renewal Queue and follows the **Open quote**
  link, which passes `policyNumber` in the route.
- The user opens a bookmarked or emailed URL of the form
  `/Renewal/Quote/{policyNumber}`.
- The default route `Home/Index` redirects to the Renewal Queue, so the quote
  screen is never the application start page.
- The action requires a policy number from the route or from `WebContext`. When
  neither supplies one, it returns the `InvalidContext` state view.

## Fields

| Field | Binds to | Control | Purpose | Validation |
| --- | --- | --- | --- | --- |
| Policy number | `RenewalQuoteViewModel.PolicyNumber` | `Html.DisplayFor` | Identifies the policy being renewed | None; display only |
| Employer | `RenewalQuoteViewModel.EmployerName` | `Html.DisplayFor` | Insured employer of record | None; display only |
| Renewal effective date | `RenewalQuoteViewModel.EffectiveDate` | Kendo DatePicker (`Html.Kendo().DatePickerFor`) | Start of the renewal term | `[Required]` and `[Remote("ValidateEffectiveDate", "Renewal")]`; client-side yes |
| Experience modifier | `RenewalQuoteViewModel.ExperienceModifier` | Kendo NumericTextBox (`Html.Kendo().NumericTextBoxFor`) | Modifier applied to the manual premium | `[Range(0.50, 2.50)]`; client-side yes |
| Quote lines | `RenewalQuoteViewModel.QuoteLines` | Kendo Grid (`Html.Kendo().Grid<QuoteLineViewModel>()`) | Calculated and adjusted premium for each class code | `QuoteLineViewModelValidator` requires a non-negative adjusted premium; client-side no |
| Adjustment reason | `RenewalQuoteViewModel.AdjustmentReason` | `Html.TextAreaFor` | Explains why any line differs from the calculated premium | `[StringLength(500)]`; required by `RenewalQuoteViewModelValidator` when a reduction exceeds 25 percent; client-side no |
| Total premium | `RenewalQuoteViewModel.TotalPremium` | `Html.DisplayFor`, formatted as currency | Sum of the adjusted line premiums | None; display only |
| Save quote | None | `<button type="submit">` `btnSaveQuote` | Posts the adjusted quote | Hidden when `IsReadOnly` |
| Issue offer | None | `<button type="submit">` `btnIssueOffer`, in a form that posts to `IssueOffer` | Issues the renewal offer for the saved quote | Hidden when `IsReadOnly` |

## Operations

| Operation | Trigger | Request | Description | Delegates to |
| --- | --- | --- | --- | --- |
| load-quote | Page load | `GET Renewal/Quote` | Reads the renewal quote for the policy number and populates the header fields and the quote lines grid | `GetRenewalQuote` in `SAIF.Policy.Services`† |
| save-quote | `btnSaveQuote` click | `POST Renewal/Quote` | Re-rates the adjusted quote lines and saves the quote, then redisplays the screen with the new total | [RecalculateRenewalPremium](../libraries/saif-renewals-rating/operations/recalculate-renewal-premium.md) |
| issue-offer | `btnIssueOffer` click | `POST Renewal/IssueOffer` | Issues the renewal offer to the employer and redirects to the Renewal Queue | [Renewal offer issuance](../application/operations/renewal-offer-issuance.md) |
| open-quote-help | `lnkQuoteHelp` click | `GET Renewal/QuoteHelp` | Returns the quote help partial | — |

## Dialogs and partials

| Name | Opens as | Returned by | Purpose |
| --- | --- | --- | --- |
| `_QuoteHelp` | kendoWindow, content loaded from the action URL | `GET Renewal/QuoteHelp` | Explains each premium line type and the limits on adjusting a line |

## AJAX and grid actions

| Request | Kind | Called by | Returns |
| --- | --- | --- | --- |
| [`POST Renewal/QuoteLines_Read`](../application/hosted-endpoints.md#post-renewalquotelines_read) | grid read | `quoteLinesGrid` data source | `DataSourceResult` of `QuoteLineViewModel` |
| [`POST Renewal/QuoteLines_Update`](../application/hosted-endpoints.md#post-renewalquotelines_update) | grid update | `quoteLinesGrid` inline edit | `DataSourceResult` with the updated line, or model state errors |
| [`GET Renewal/ValidateEffectiveDate`](../application/hosted-endpoints.md#get-renewalvalidateeffectivedate) | remote validator | `EffectiveDate` field | JSON `true`, or an error message string |

## Client-side behavior
- **Script module:** `Scripts/app/renewal/quote.js`
- `quoteLinesGrid` is a Kendo Grid with inline editing; after each
  `QuoteLines_Update` response the `quoteViewModel` `kendo.observable` view model
  recomputes `totalPremium`.
- The `EffectiveDate` DatePicker uses the policy expiration date as its minimum
  and relies on jQuery unobtrusive validation for the remote check.
- `lnkQuoteHelp` opens `_QuoteHelp` in a kendoWindow.
- Failed grid requests raise a toastr notification.

## States

| State view | Condition |
| --- | --- |
| `NoQuote` | `GetRenewalQuote` returns no quote because the policy is outside its renewal window |
| `InvalidContext` | No policy number can be resolved from the route or `WebContext` |

## Business rules

| Rule ID | Rule | Enforcement | Source |
| --- | --- | --- | --- |
| [BR-SCR-001](../application/business-rules.md#br-scr-001-effective-date-follows-expiration) | The renewal effective date must be the day after the current policy expiration date | `ValidateEffectiveDate` remote validator and `RenewalQuoteViewModelValidator` | `src/SAIF.Policy.Renewals.Web/Controllers/RenewalController.cs:131` |
| [BR-SCR-002](../application/business-rules.md#br-scr-002-large-reductions-need-a-reason) | A quote line cannot be reduced by more than 25 percent of its calculated premium without an adjustment reason | `RenewalQuoteViewModelValidator` | `src/SAIF.Policy.Renewals.Web/Validators/RenewalQuoteViewModelValidator.cs:22` |
| [BR-SCR-003](../application/business-rules.md#br-scr-003-offers-issued-by-underwriting-only) | Only underwriting staff can issue a renewal offer | `[SaifAuthorize]` on `IssueOffer` | `src/SAIF.Policy.Renewals.Web/Controllers/RenewalController.cs:98` |
| [BR-APP-002](../application/business-rules.md#br-app-002-quote-requires-renewal-window) | A renewal quote is available only for a policy inside its renewal window | `NoQuote` state view returned by `Quote` | `src/SAIF.Policy.Renewals.Web/Controllers/RenewalController.cs:58` |

## Navigation

| Target | Trigger | Parameters |
| --- | --- | --- |
| [Renewal Queue](renewal-queue.md) | **Back to queue** link; redirect after **Issue offer** | None |
| [Quote History](quote-history.md) | **History** link | `policyNumber` route parameter |
| Policy Summary in the Policy Services Portal | **Policy summary** link | `policyNumber` carried in the web context; the portal URL is held in the `PolicyPortalUrl` app setting |

## Screen flow diagram

```mermaid
flowchart TD
    A[Renewal Quote] -->|Back to queue or Issue offer| B[Renewal Queue]
    A -->|History| C[Quote History]
    A -->|Policy summary| D[Policy Summary in the Policy Services Portal]
    B -->|Open quote| A
```

---

> [!NOTE]
> Statements marked with `*` are inferred from code patterns. Statements marked with `**` could not be verified from available source. Statements marked with `†` are sourced from package documentation retrieved with the Legends CLI.

## Sources
- src/SAIF.Policy.Renewals.Web/Controllers/BaseController.cs
- src/SAIF.Policy.Renewals.Web/Controllers/RenewalController.cs
- src/SAIF.Policy.Renewals.Web/Views/Renewal/Quote.cshtml
- src/SAIF.Policy.Renewals.Web/Views/Renewal/_QuoteHelp.cshtml
- src/SAIF.Policy.Renewals.Web/Models/RenewalQuoteViewModel.cs
- src/SAIF.Policy.Renewals.Web/Models/QuoteLineViewModel.cs
- src/SAIF.Policy.Renewals.Web/Validators/RenewalQuoteViewModelValidator.cs
- src/SAIF.Policy.Renewals.Web/Scripts/app/renewal/quote.js
````
