Views
When to use this
Views are ideal for multi-step workflows—dashboards, menus, or game scenes—where you want to encapsulate options and state per screen. They integrate prompts with lifecycle events so you can build entire applications from composable classes.
Quick start
using ConsoulLibrary;
Consoul.View("Tools")
.Option("Say hello", () =>
{
Consoul.Write("Hello!");
Consoul.Wait();
})
.Render();
Select an action by number; enter back to leave the view. Start with the complete menu recipe, then use model-backed sections as the app grows.
Key types
StaticView– Presents a fixed set of options defined via attributes or collection initialisation.DynamicView– Rebuilds its option list every time it renders, enabling state-aware labels and colours.ViewOption/DynamicOption<T>– Describe the work performed when a user selects an option.ChoiceCallback– Optional delegate that receives notification whenever an option is executed.
Minimal example
[View("Main menu")]
public class MainMenu : StaticView
{
public MainMenu() : base()
{
}
[ViewOption("Start job", ConsoleColor.Green)]
private void StartJob()
{
Consoul.Write("Starting job...", ConsoleColor.Green);
GoBack();
}
[ViewOption("Show high scores", ConsoleColor.Cyan)]
private void ShowScores()
{
Consoul.Write("Loading scores...", ConsoleColor.Yellow);
NavigateTo<HighScoresView>(replace: false);
}
[ViewOption("Exit", ConsoleColor.DarkGray)]
private void Exit()
{
Consoul.Write("Goodbye!", ConsoleColor.Gray);
GoBack();
}
}
Navigation helpers
StaticView and DynamicView<T> expose protected helpers to request navigation without manually instantiating other views. Call NavigateTo<TView>() to replace the current view (the default), NavigateTo<TView>(replace: false) to push a new view on the stack, NavigateTo(() => new DetailView(arg)) when you need to supply constructor parameters, or GoBack() to pop to the previous screen. Constructor-style DynamicView<T> POC menus can expose the same flow with Navigate<TView>("Details", () => new DetailView(model), replace: false) or the StaticView-shaped AddNavigationOption<TView>(...) alias. Constructor-defined StaticView menus can use AddNavigationOption<TView>("Details", () => new DetailView(model), replace: false) when the destination needs injected state. The renderer executes these requests inside an iterative loop, preventing recursive render chains and the stack overflows that could occur when options previously invoked new OtherView().Render().
Advanced tips
- Attribute-driven options – Decorate methods with
ViewOptionAttributeorDynamicViewOptionAttributeto declare choices alongside their logic. Attribute metadata can control colour, descriptions, shortcuts, and hidden search aliases; useViewOptionAttribute.SearchTermsorDynamicViewOptionAttribute.SearchTermsMethodwhen a controller-style action label should stay short but still be findable by domain words. - POC model construction – Use
Consoul.View("Title", model)when a screen owns an existing model instance, orConsoul.View<TModel>("Title")when a parameterless model is enough for a first sketch. Both paths returnDynamicView<TModel>, so the screen still renders model state and treats options as controller-style actions. - State-aware options – Use
DynamicView<T>.OptionWhen(...)when a model action should remain visible but disabled until the current model satisfies a predicate. UseDynamicView<T>.OptionWhenValid(...)for save, publish, deploy, or apply actions that should wait for DataAnnotations-style model validation. The option is re-evaluated on each render and shows the disabled reason through the normal prompt renderer. Async POC handlers can useFunc<Task>orFunc<TModel, Task>overloads when they do not need the cancellation token. - Confirmed actions – Use
DynamicView<T>.ConfirmOption(...),DynamicView<T>.AddConfirmOption(...), orStaticView.AddConfirmOption(...)for save, delete, deploy, and other guarded actions. They use the sameConfirmResultsemantics asConsoul.Confirm(...), so canceled or declined confirmations leave the action untouched and return to the view loop. Async POC handlers can also useFunc<Task>,Func<CancellationToken, Task>,Func<TModel, Task>, orFunc<TModel, CancellationToken, Task>overloads when they do not need all parameters. - Prompt ergonomics – Set
PromptKeyboardNavigationEnabled,PromptFilterEnabled, orPromptPageSizeon aStaticVieworDynamicView<T>when a POC menu needs arrow navigation, typed filtering, or paging without manually constructing a prompt. UseStaticView.AddOption(...)for constructor-defined sync or async actions,StaticView.AddConfirmOption(...)for guarded static actions,StaticView.AddNavigationOption<TView>(...)for typed or factory navigation actions,DynamicView<T>.AddOption(...),DynamicView<T>.AddConfirmOption(...),DynamicView<T>.AddNavigationOption(...),DynamicView<T>.AddEditFormOption(...),DynamicView<T>.Separator(...),StaticView.AddSeparator(...), orViewOption.Separator(...)to group larger menus without changing selectable option indexes. The dynamic add-style names are aliases overOption(...),ConfirmOption(...),Navigate(...), andEditForm(...), preserving the same MVC-style model-backed view rather than introducing a separate fluent view or form view layer. UseDynamicView<T>.AddOptions(...)when a model-backed POC menu should project a collection of action descriptors into selectable controller actions without hand-written loops; optionalisEnabledanddisabledReasonselectors keep projected actions visible but unavailable until the view model is ready. Navigation options can target a view type or a factory such as() => new DetailView(model)when the destination needs constructor parameters. These APIs acceptConsoleColorvalues, reusableStyleobjects, style strings such as"green","grey","aqua", and"#22c55e", directColor/RgbColorvalues for constructor-defined static menus, dynamic menus, and named sections,Styleselectors such asmodel => new Style(model.Accent, Decoration.Bold), orColor/RgbColorselectors when the model carries the palette. Style metadata is carried through the view prompt, command palette, generated help, and section titles on ANSI-capable terminals instead of being flattened to the nearest console color. UseDynamicView<T>.DescribeLastOption(...)orViewOption.Describe(...)to add searchable helper text under options while keeping controller-style action labels short. UseDynamicView<T>.SearchLastOption(...)orViewOption.AddSearchTerms(...)for hidden aliases likeship,publish, orprod. UseDynamicView<T>.ShortcutLastOption(...),ViewOption.UseShortcut(...), orViewOptionAttribute.Shortcutwhen frequent actions should be selectable by a typed or keyboard shortcut. - Generated help – Use
DynamicView<T>.HelpOption(...)orStaticView.AddHelpOption(...)when exploratory users need an in-app reference for a POC menu. The generatedViewHelpEntryuses the current view’s labels, descriptions, shortcuts, separators, disabled reasons, and Go Back action, andCreateHelp(...)returns the same help surface for panels, layouts, captures, or tests. Help, command palette, and view-help entries accept reusableStyleobjects, style strings,Color, andRgbColorvalues, so constructor-defined static menus and model-backed dynamic menus can share the same palette.ViewHelpEntryandViewHelpOptionspreserve RGB/hex colors and ANSI decorations for labels, separators, title rules, shortcuts, descriptions, and disabled-only rows on capable terminals while falling back to readable console colors. Help labels, descriptions, disabled reasons, and separator labels can contain raw markup and still wrap by visible text, while shortcut text remains literal. - View chrome – Use
ShowBreadcrumbs()andShowShortcuts()onDynamicView<T>orStaticViewwhen a multi-screen POC app needs lightweight navigation context. Breadcrumb chrome is populated fromViewRenderernavigation stack entries created byNavigateTo(...), while shortcut chrome reuses existing shortcut metadata plus the generated Go Back action. - Command palette – Use
DynamicView<T>.CommandPaletteOption(...)orStaticView.AddCommandPaletteOption(...)when view actions should be searchable. Pass aStyleobject, style string,Color, orRgbColorwhen a POC view uses a shared palette. The palette reuses option labels, descriptions, shortcuts, hidden search aliases, separators, disabled reasons, and filtering behavior, then invokes the selected action and returns to the same view loop. - View-local errors – Use
ThrowOnError(),RenderErrorsAndContinue(),UseErrorMode(...), orWaitOnRenderError(...)on aStaticVieworDynamicView<T>when one MVC-style screen needs different behavior from globalRenderOptions.ViewErrorModeorRenderOptions.WaitOnError. This keeps permissive exploratory menus and stricter controller-style views in the same application without changing process-wide defaults. - Go back behaviour – Each view automatically appends a “Go back” option and now uses
PromptResultcancelation rather than the legacyConsoul.EscapeIndexconstant. Customise the label viaViewAttribute.GoBackMessageor by changingRenderOptions.DefaultGoBackMessage, and checkGoBackRequestedafterRenderAsyncreturns to run cleanup when a user leaves mid-operation. - Async rendering – Use
RenderAsync()on dynamic views when options trigger asynchronous operations; the framework awaits each action before re-rendering. Combine this withCancellationTokenparameters on option handlers to respond to user aborts or window closures, and surface progress updates by re-rendering interim messages viaConsoul.Write. - Shared state & navigation – Pass dependencies via constructors or properties; views are regular classes and can store fields, timers, or other stateful components. Use
NavigateTo<TView>()(orNavigateTo<TView>(replace: false)when you need to push) orGoBack()inside an option to request transitions; the renderer now applies these commands iteratively so the call stack stays flat even through long navigation chains. Inject shared services (logging, data providers) to keep options thin and testable. - Composed dashboards –
DynamicViewoptions can render bar charts, breakdown charts, calendars, columns, exception diagnostics, grids, headlines, property lists, sparklines, spinners, tables, text blocks, trees, panels, or progress bars inline, then rehydrate their options list after completing background work. This makes it easy to build monitoring dashboards driven by timers or file watchers; schedule refreshes withSystem.Threading.Timerto triggerRenderAsync()when data changes. - Reusable model sections –
DynamicView<T>.RenderModel(...)accepts either the original write action or a reusableIConsoulRenderable/model-to-renderable factory. UseDynamicView<T>.Section(...)to append named MVC-style dashboard sections after the primary model renderer and before the option prompt; section titles are re-evaluated on every render, so mutated model state appears immediately after an action completes. UseDynamicView<T>.PropertyListSection(...)for a generated read-only model summary,DynamicView<T>.TableSection(...)for a generated responsive table from a collection owned by the view model,DynamicView<T>.MetricsSection(...)for compact KPI or health-state values,DynamicView<T>.RowsSection(...),ColumnsSection(...), andGridSection(...)for composed dashboard rows, side-by-side summaries, or responsive cells owned by the view model,DynamicView<T>.LayoutSection(...)for responsive dashboard panes or nested layout regions owned by the view model,DynamicView<T>.TextBlockSection(...)for wrapped explanatory text owned by the view model,DynamicView<T>.PanelSection(...)for framed detail text owned by the view model,DynamicView<T>.CalloutSection(...)for status/warning/success/error messages owned by the view model,DynamicView<T>.ExceptionSection(...)for a last error or diagnostic exception owned by the view model,DynamicView<T>.BreadcrumbSection(...)for model-owned navigation context,DynamicView<T>.ShortcutBarSection(...)for model-owned action hints or shortcut metadata,DynamicView<T>.BulletListSection(...)for notes or nested diagnostics,DynamicView<T>.ChecklistSection(...)for readiness or workflow checks,DynamicView<T>.CalendarSection(...)for schedule state,DynamicView<T>.SparklineSection(...)for trend values,DynamicView<T>.BarChartSection(...)/BreakdownChartSection(...)for responsive chart rows,DynamicView<T>.TreeSection(...)for nested dependency or diagnostic state, andDynamicView<T>.TimelineSection(...)for workflow or history steps projected from the model. UseDynamicView<T>.SectionWhen(...),PropertyListSectionWhen(...),TableSectionWhen(...),MetricsSectionWhen(...),RowsSectionWhen(...),ColumnsSectionWhen(...),GridSectionWhen(...),LayoutSectionWhen(...),TextBlockSectionWhen(...),PanelSectionWhen(...),CalloutSectionWhen(...),ExceptionSectionWhen(...),BreadcrumbSectionWhen(...),ShortcutBarSectionWhen(...),BulletListSectionWhen(...),ChecklistSectionWhen(...),CalendarSectionWhen(...),SparklineSectionWhen(...),BarChartSectionWhen(...),BreakdownChartSectionWhen(...),TreeSectionWhen(...),TimelineSectionWhen(...), orValidationSummarySectionWhen(...)for validation, warnings, generated summaries, advanced details, or other model sections that should appear only after state makes them relevant. Section title colors acceptConsoleColor, reusableStyleobjects, style strings, directColor/RgbColorvalues, or model selectors returningColor/RgbColor, andSection(...)/SectionWhen(...)also acceptJustify.Left,Justify.Center, andJustify.Rightfor copied rule-title alignment snippets. That keeps quick dashboards visually clear without introducing a second view abstraction. UseConsoul.Renderable(() => { ... })when a one-off render action should participate in that reusable component flow without creating a custom entry class. - Scoped themes – Wrap a view render, dashboard section, or sample workflow in
using (Consoul.UseTheme(ConsoulTheme.Modern))to preview a cohesive style without permanently changing application-wide render options. - Choice callbacks – Supply a
ChoiceCallbackto track navigation analytics, enforce authorisation, or log audit trails. Because the callback receives the executed option, you can centralise cross-cutting concerns instead of duplicating logic inside every handler.
Form and object editing
Consoul.Form(...), DynamicView<T>.EditForm(...), DynamicView<T>.AddEditFormOption(...), StaticView.AddEditFormOption(...), and EditObjectView all use the same object-editor stack behind quick settings editors, exploratory object editors, and the earlier JSON-view work. The JSON terminology remains only as compatibility aliases for the same form document editor. It renders the target model as an annotated form document so users can arrow through property values before pressing Enter to edit. The value layout is JSON-style for scanability, and each line includes JavaScript-style comments sourced from [Display] metadata and the model’s XML documentation (including <see cref="..."/> references). Use the left/right or up/down arrows—or the Tab key—to change the selection, and Esc to return to the menu.
For model-backed POC screens, call .PropertyListSection("Summary") for the common read-only model summary, .RenderModel(model => new PropertyListEntry(...).AddModel(model)) when a custom summary object is clearer, and .EditForm("Edit settings") or .AddEditFormOption("Edit settings") to add the same editor as a menu option. For named static menus, store or inject the settings object and call AddEditFormOption("Edit settings", settings). Use Consoul.Form(model).Render() only when you want to open the editor directly without a surrounding view. EditModel(...) and Consoul.EditObject(...) remain available as compatibility names for callers that already use object-editor terminology.
Use Consoul.PropertyList(model) for read-only summaries and DynamicView<T>.ValidationSummarySection(..., title: "Validation") before save actions. Use ValidationSummarySectionWhen(...) for post-action checks or advanced validation details that should appear only after model state makes them relevant. Both helpers reuse the same model metadata patterns as form editing, so POC views can render, validate, edit, and guard actions on one model without hand-wiring every property. [Display(Order = ...)] controls generated property order, and generated object-editor menus group related fields with [Display(GroupName = "...")] or [Category("...")]. [Browsable(false)], [Display(AutoGenerateField = false)], [ScaffoldColumn(false)], and [PropertyEditorIgnore] hide properties from generated model surfaces, while [ReadOnly(true)] and [Editable(false)] remove properties from edit menus but keep them available for read-only summaries. Validation summaries include property attributes and object-level IValidatableObject results, so cross-field checks also disable OptionWhenValid(...) actions. ValidationSummaryOptions.InvalidColor(...), IssueColor(...), ValidColor(...), and ValidMessageColor(...) accept ConsoleColor, reusable Style objects, or Spectre-style strings for the same palette used by prompts and status widgets. When validation output belongs in a reusable dashboard section, return or compose a ValidationSummaryEntry the same way you would a property list, table, panel, or chart.
Metadata-aware prompts
When a property is selected the editor surfaces the resolved display name, description and summary before invoking an appropriate prompt. Generated property menu options also render those descriptions and enable filtering by default, so users can type /owner, /path, or another hint from metadata to find a field quickly. For simple text and numeric types the default prompt uses TextPrompt<T> so the property’s current value is available as the Enter default, cancellation leaves the property unchanged, and DataAnnotations-style validation is enforced while the user edits. Frequent rules such as [Required], [Range], [StringLength], [MinLength], and [MaxLength] get purpose-built prompt hints, while other ValidationAttribute types such as [RegularExpression], [EmailAddress], [Compare], or custom attributes are invoked through context-aware validation when available. Boolean properties use ConfirmPrompt, and enum properties use metadata-backed selection prompts with [Display], [DisplayName], or [Description] labels. Nullable boolean and enum properties use clearable selection prompts so users can intentionally unset optional values; [Required] suppresses that clear choice. String properties ending with Path automatically use Consoul.PromptForFilepath. Built-in editors, nested editors, and custom IPropertyEditor implementations receive the active render token through PropertyEditContext.CancellationToken. You can opt into other editors by decorating properties with PropertyEditorAttribute:
public sealed class AdapterConfiguration
{
[PropertyEditor(typeof(FilePathPropertyEditor))]
[Display(Name = "Adapter", Description = "Full path to the adapter implementation.")]
public string AdapterPath { get; set; } = string.Empty;
}
If you need to normalise values before assignment—for example trimming a partial path—implement IPropertyValueFormatter and apply PropertyValueFormatterAttribute to the property. Formatters receive the edit context so they can inspect the model, documentation and original value:
public sealed class RelativePathFormatter : IPropertyValueFormatter
{
public object Format(PropertyEditContext context, object value)
{
if (value is string text)
{
return Path.GetRelativePath(AppContext.BaseDirectory, text);
}
return value;
}
}
public sealed class ScriptOptions
{
[PropertyEditor(typeof(FilePathPropertyEditor))]
[PropertyValueFormatter(typeof(RelativePathFormatter))]
public string ScriptPath { get; set; } = string.Empty;
}
Complex types (objects, collections and dictionaries) still launch nested EditObjectView instances so you can drill into hierarchies while preserving the legacy menu options for power users.
Metadata resolvers
Some properties surface values whose structure is only known at runtime—for example, dictionaries that map to constructor parameters in an external assembly. Decorate these members with PropertyMetadataResolverAttribute to plug in a custom resolver that supplies documentation, editors or formatters without relying on reflection against the runtime value.
public sealed class OptionsMetadataResolver : IPropertyMetadataResolver
{
public ResolvedPropertyMetadata Resolve(PropertyInfo property)
{
var documentation = new PropertyDocumentation(
property,
"Options",
"Key/value pairs configuring the remote adapter.",
() => "Each key should match a constructor parameter on the adapter type.");
return new ResolvedPropertyMetadata(
documentation,
new DefaultPropertyEditor(),
null);
}
}
public sealed class AdapterConfiguration
{
[PropertyMetadataResolver(typeof(OptionsMetadataResolver))]
public Dictionary<string, object> Options { get; set; } = new Dictionary<string, object>();
}
Resolvers can construct rich guidance even when the property value references remote types, enabling consistent prompts and comments throughout the form document editor.
Layered editors
Some data surfaces as opaque object graphs—think JSON blobs, plugin settings loaded from another assembly, or dictionaries whose values describe constructor parameters. To keep these scenarios discoverable the resolver can also expose additional layers through IPropertyLayerProvider. Each layer describes a named editing surface with an optional description and a handler that receives the active PropertyEditContext. When the user presses Enter on the property, Consoul prompts whether to run the default editor or one of the custom layers.
Implementations can parse JSON Schema files, spin up another EditObjectView, or invoke bespoke editors for nested structures. The handler can mutate context.CurrentValue and return true to signal that the new value should be applied automatically. If the layer wants to persist the value directly it can call context.ApplyValue and still return true while marking the PropertyEditLayer as not applying the context value. Returning false now falls back to the default editor so layers can bootstrap data (for example by materialising dictionary entries) and then let the built-in prompts continue the session. Use context.CancellationToken when a layer renders prompts or nested editors so a canceled MVC-style view exits the whole object-edit flow cleanly.
public sealed class OptionsMetadataResolver : IPropertyMetadataResolver
{
public ResolvedPropertyMetadata Resolve(PropertyInfo property)
{
var documentation = new PropertyDocumentation(
property,
"Options",
"Key/value pairs configuring the remote adapter.",
() => "Each key should match a constructor parameter on the adapter type.");
return new ResolvedPropertyMetadata(
documentation,
new DefaultPropertyEditor(),
null,
new SchemaLayerProvider());
}
}
private sealed class SchemaLayerProvider : IPropertyLayerProvider
{
public IEnumerable<PropertyEditLayer> GetLayers(PropertyEditContext context)
{
return new[]
{
new PropertyEditLayer(
"Edit via schema",
"Loads the remote JSON schema and opens a nested editor for the selected dictionary entry.",
RunSchemaEditor,
true)
};
}
private static bool RunSchemaEditor(PropertyEditContext context)
{
// Locate the JSON schema based on the current model and open a tailored view.
var view = new EditObjectView(JsonSchemaParser.BuildModel(context.CurrentValue));
view.RenderAsync(context.CancellationToken).GetAwaiter().GetResult();
context.CurrentValue = view.Model;
return true; // the updated model will be applied by Consoul.
}
}
Because layers participate in the same prompt flow, users can switch between schema-driven editors, remote type reflection and the built-in prompts without leaving the form document workspace.