Deprecated. Moved to #Script Pages UI Controls and Razor UI Controls dedicated docs.
Both #Script Pages and ServiceStack.Razor Share the same implementations for their Server Controls which are utilized in ASP.NET Core Project Templates and World Validation Application.
UI Component List
Currently the component libraries include common Bootstrap UI Form Controls and Navigation Components, here's a side-by-side comparison displaying the names for the different Control for each Server Control:
Control | #Script Pages | ServiceStack.Razor |
---|---|---|
ErrorSummary | validationSummary | @Html.ValidationSummary |
ValidationSuccess | validationSuccess | @Html.ValidationSuccess |
Input | formInput | @Html.FormInput |
TextArea | formTextarea | @Html.FormTextarea |
Select | formSelect | @Html.FormSelect |
CheckBox | formInput({type:'checkbox'}) | @Html.FormInput(new { type = "checkbox"}) |
HiddenInputs | htmlHiddenInputs | @Html.HiddenInputs |
SvgImage | svgImage | @Html.SvgImage |
Nav | nav | @Html.Nav |
Navbar | navbar | @Html.Navbar |
NavLink | navLink | @Html.NavLink |
NavButtonGroup | navButtonGroup | @Html.NavButtonGroup |
Bootstrap UI Form Controls
The Bootstrap UI form controls include built-in support for validation where they can render validation errors from ServiceStack's ResponseStatus
object, e.g the Login Page in World Validation:
#Script
Pages
<form action="/auth/credentials" method="post" class="col-lg-4">
<div class="form-group">
{{ ['userName','password'] | validationSummary({class:'alert alert-warning'}) }}
{{ { continue: qs.continue ?? '/server/', errorView:'/server/login' } | htmlHiddenInputs }}
</div>
<div class="form-group">
{{ {id:'userName'}
| formInput({label:'Email',help:'Email you signed up with',size:'lg'}) }}
</div>
<div class="form-group">
{{ {id:'password',type:'password'}
| formInput({label:'Password',help:'6 characters or more',size:'lg',preserveValue:false}) }}
</div>
<div class="form-group">
{{ {id:'rememberMe',type:'checkbox',checked:true} | formInput({label:'Remember Me'}) }}
</div>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
<div class="form-group">
<a class="lnk" href="/server/register">Register New User</a>
</div>
</form>
ServiceStack.Razor
The equivalent implementation in Razor:
<form action="/auth/credentials" method="post" class="col-lg-4">
<div class="form-group">
@Html.ValidationSummary(new[]{ "userName","password" },
new { @class = "alert alert-warning" })
@Html.HiddenInputs(new {
@continue = Html.Query("continue") ?? "/server-razor/",
errorView = "/server-razor/login"
})
</div>
<div class="form-group">
@Html.FormInput(new { id = "userName" }, new InputOptions {
Label = "Email",
Help = "Email you signed up with",
Size = "lg",
})
</div>
<div class="form-group">
@Html.FormInput(new { id = "password", type = "password" }, new InputOptions {
Label = "Password",
Help = "6 characters or more",
Size = "lg",
PreserveValue = false,
})
</div>
<div class="form-group">
@Html.FormInput(new {
id = "rememberMe",
type = "checkbox",
@checked = true,
},
new InputOptions { Label = "Remember Me" })
</div>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
<div class="form-group">
<a class="lnk" href="/server-razor/register">Register New User</a>
</div>
</form>
Login Page UI
The Login Page contains a standard Bootstrap Username/Password form with labels, placeholders and help text, which initially looks like:
What it looks like after submitting an empty form with Server Exception Errors rendered against their respective fields:
Form Control Properties
Essentially both #Script
and Razor
have identical properties but implemented idiomatically for each control where #Script
uses camelCase names and JS Object literals. The first (aka target) argument is for attributes you want to add to the HTML <input/>
Element whilst the 2nd Argument is used to customize any of its other high-level features:
/// High-level Input options for rendering HTML Input controls
public class InputOptions
{
/// Display the Control inline
public bool Inline { get; set; }
/// Label for the control
public string Label { get; set; }
/// Class for Label
public string LabelClass { get; set; }
/// Override the class on the error message (default: invalid-feedback)
public string ErrorClass { get; set; }
/// Small Help Text displayed with the control
public string Help { get; set; }
/// Bootstrap Size of the Control: sm, lg
public string Size { get; set; }
/// Multiple Value Data Source for Checkboxes, Radio boxes and Select Controls
public object Values { get; set; }
/// Typed setter of Multi Input Values
public IEnumerable<KeyValuePair<string, string>> InputValues
{
set => Values = value;
}
/// Whether to preserve value state after post back
public bool PreserveValue { get; set; } = true;
/// Whether to show Error Message associated with this control
public bool ShowErrors { get; set; } = true;
}
Contacts Page
The Contacts Page shows a more complete example with a number of different UI Controls.
#Script Pages
<form action="/contacts" method="post" class="col-lg-4">
<div class="form-group">
{{ 'title,name,color,age,filmGenres,agree' | validationSummary }}
</div>
<div class="form-group">
{{ {id:'title',type:'radio'} | formInput({values:contactTitles,inline:true}) }}
</div>
<div class="form-group">
{{ {id:'name',placeholder:'Name'} | formInput({label:'Full Name',help:'Your first and last name'}) }}
</div>
<div class="form-group">
{{ {id:'color',class:'col-4'}
| formSelect({label:'Favorite color',values:{'', ...contactColors}}) }}
</div>
<div class="form-group">
{{ {id:'filmGenres',type:'checkbox'}
| formInput({label:'Favorite Film Genres',values:contactGenres,help:"choose one or more"}) }}
</div>
<div class="form-group">
{{ {id:'age',type:'number',min:13,placeholder:'Age',class:'col-3'} | formInput }}
</div>
<div class="form-group">
{{ {id:'agree',type:'checkbox'} | formInput({label:'Agree to terms and conditions'}) }}
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a href="/server/contacts/">reset</a>
</div>
</form>
Whereas Razor uses anonymous objects for properties that can be unbounded like a HTML Element Attribute List and a Typed Class like InputOptions
to specify the controls other high-level features, e.g:
ServiceStack.Razor
<form action="/contacts" method="post" class="col-lg-4">
<div class="form-group">
@Html.ValidationSummary(new[]{ "title","name","color","age","filmGenres","agree" })
@Html.HiddenInputs(new { @continue = Continue, errorView = Continue })
</div>
<div class="form-group">
@Html.FormInput(new {
id = "title",
type = "radio",
}, new InputOptions {
Values = Html.ContactTitles(),
Inline = true,
})
</div>
<div class="form-group">
@Html.FormInput(new {
id = "name",
placeholder = "Name",
}, new InputOptions {
Label = "Full Name",
Help = "Your first and last name",
})
</div>
<div class="form-group">
@Html.FormSelect(new {
id = "color",
@class = "col-4",
}, new InputOptions {
Label = "Favorite color",
Values = new StringDictionary { {"",""} }.Merge(Html.ContactColors()),
})
</div>
<div class="form-group">
@Html.FormInput(new {
id = "filmGenres",
type = "checkbox",
}, new InputOptions {
Label = "Favorite Film Genres",
Help = "choose one or more",
Values = Html.ContactGenres()
})
</div>
<div class="form-group">
@Html.FormInput(new {
id = "age",
type = "number",
min = 13,
placeholder = "Age",
@class = "col-3",
})
</div>
<div class="form-group">
@Html.FormInput(new {
id = "agree",
type = "checkbox",
},
new InputOptions { Label = "Agree to terms and conditions" })
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a href="/server-razor/contacts/">reset</a>
</div>
</form>
Both Server UI Controls provide auto Validation Form Binding for any validation rules specified on the CreateContact
Validator:
public class CreateContactValidator : AbstractValidator<CreateContact>
{
public CreateContactValidator()
{
RuleFor(r => r.Title).NotEqual(Title.Unspecified).WithMessage("Please choose a title");
RuleFor(r => r.Name).NotEmpty();
RuleFor(r => r.Color).Must(x => x.IsValidColor()).WithMessage("Must be a valid color");
RuleFor(r => r.FilmGenres).NotEmpty().WithMessage("Please select at least 1 genre");
RuleFor(r => r.Age).GreaterThan(13).WithMessage("Contacts must be older than 13");
RuleFor(x => x.Agree).Equal(true).WithMessage("You must agree before submitting");
}
}
As well as any ArgumentException
thrown within the Service Implementation:
public object Any(CreateContact request)
{
var newContact = request.ConvertTo<Data.Contact>();
newContact.Id = Interlocked.Increment(ref Counter);
newContact.UserAuthId = this.GetUserId();
newContact.CreatedDate = newContact.ModifiedDate = DateTime.UtcNow;
var contacts = Contacts.Values.ToList();
var alreadyExists = contacts.Any(x => x.UserAuthId == newContact.UserAuthId && x.Name == request.Name);
if (alreadyExists)
throw new ArgumentException($"You already have contact named '{request.Name}'",nameof(request.Name));
Contacts[newContact.Id] = newContact;
return new CreateContactResponse { Result = newContact.ConvertTo<Contact>() };
}
Contacts Page UI
The Contacts Page is representative of a more complex page that utilizes a variety of different form controls where the same page is also responsible for rendering the list of existing contacts:
Here's an example of what a partially submitted invalid form looks like:
To view the complete implementation in context checkout World Validation Server Implementation.
Navigation Controls
The Server Navigation Controls are used to render your Apps Unified Navigation
#Script
Pages
In #Script Pages you can use render the navbar
and navButtonGroup
methods to render NavItems:
Navbar
You can render the main menu navigation using the navbar script method:
{{ navbar }}
Which by default renders the View.NavItems
main navigation, using the default NavOptions
and User Attributes (if authenticated):
You can also render a different Navigation List with:
{{ navItems('submenu').navbar() }}
Which can be customized using the different NavOptions
properties above, in camelCase:
{{ navItems('submenu').navbar({ navClass: 'navbar-nav navbar-light bg-light' }) }}
Button group
The navButtonGroup
script can render NavItems in a button group, e.g. the OAuth buttons are rendered with:
{{ 'auth'.navItems().navButtonGroup({ navClass: '', navItemClass: 'btn btn-block btn-lg' }) }}
Which renders a vertical, spaced list of buttons which look like:
Razor Pages
The same server controls are available in ServiceStack.Razor Apps as HTML Helper extension methods:
Navbar
@Html.Navbar()
@Html.Navbar(Html.GetNavItems("submenu"))
@Html.Navbar(Html.GetNavItems("submenu"), new NavOptions {
NavClass = "navbar-nav navbar-light bg-light"
})
NavButtonGroup
@Html.NavButtonGroup(Html.GetNavItems("auth"), new NavOptions {
NavClass = "",
NavItemClass = "btn btn-block btn-lg",
})
NavOptions Properties
The NavItem
classes capture the Navigation information which is used together with the NavOptions
class below:
public class NavOptions
{
/// User Attributes for conditional rendering, e.g:
/// - auth - User is Authenticated
/// - role:name - User Role
/// - perm:name - User Permission
public HashSet<string> Attributes { get; set; }
/// Path Info that should set as active
public string ActivePath { get; set; }
/// Prefix to include before NavItem.Path (if any)
public string BaseHref { get; set; }
// Custom classes applied to different navigation elements (defaults to Bootstrap classes)
public string NavClass { get; set; }
public string NavItemClass { get; set; }
public string NavLinkClass { get; set; }
public string ChildNavItemClass { get; set; }
public string ChildNavLinkClass { get; set; }
public string ChildNavMenuClass { get; set; }
public string ChildNavMenuItemClass { get; set; }
}
Use camelCase when specifying nav options in #Script
controls.