Code Pages
Another solution that enables greater control and functionality are Code Pages which utilize the full unlimited power of the C# programming language to enable precise control over the rendering of pages and partials. As Code Pages are normal C# classes they also benefit from the rich editing and debugging experience in C# IDEs and don't require any special design-time editors, build tools or pre-compilation like Razor as they're just regular classes in your Solution.
Creating Code Pages
A Code Page is a class that inherits SharpCodePage and is annotated with a [Page(virtualPath)] attribute which is used to resolve the page in the same way .html pages are resolved by their virtualPath. Code Pages take precedence over .html pages so you can start with an .html page and when you need more power and expressiveness, create a Code Page at the same virtualPath as the page you want to replace and it will get used instead. They also follow the same layout resolution rules as normal pages so they can be easily substituted when needed.
Code Pages can also have metadata using the [PageArg] attribute which have the same behavior as args in .html pages.
render Method
The last thing a Code Page needs is a render method which is what gets called to execute the page. The render method can have any number of parameters which will be populated with arguments of the same name that's in scope at the time when the Code Page is called. This enables the nice UX where the arguments can be utilized in a C# interpolated string.
A complete CodePage implementation using all these features is below:
ProductsPage.cs
using System.Linq;
using System.Collections.Generic;
using ServiceStack;
using ServiceStack.Script;
namespace SharpScript
{
[Page("products")]
[PageArg("title", "Products")]
public class ProductsPage : SharpCodePage
{
string render(Product[] products) => $@"
<table class='table table-striped'>
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
{products.OrderBy(x => x.Category).ThenBy(x => x.ProductName).Map(x => $@"
<tr>
<th>{x.Category}</th>
<td>{x.ProductName.HtmlEncode()}</td>
<td>{x.UnitPrice:C}</td>
</tr>").Join("")}
</table>";
}
}
Where with just this implementation, it can now be called by navigating to its virtualPath:
/products
You typically wont need to explicitly register Code Pages since the SharpPagesFeature populates its ScanAssemblies with the AppHost's Service assemblies which will automatically find and register any SharpCodePage's that are in the same Assembly as ServiceStack Services and just like Services, Code Pages are autowired and resolved from the IOC transiently, so they can also be injected with any IOC registered dependencies by declaring them as public properties.
MVC Code Pages
You can also combine the functionality enabled by levaraging ServiceStack Services in MVC Pages with the precise layout and expressiveness of Code Pages, where just like you can render a Page in a PageResult, you can render a CodePage by resolving it with its virtualPath:
ProductServices.cs
using ServiceStack;
using ServiceStack.Script;
namespace SharpScript
{
[Route("/products/view")]
public class ViewProducts
{
public string Id { get; set; }
}
public class ProductsServices : Service
{
public object Any(ViewProducts request) =>
new PageResult(Request.GetCodePage("products")) {
Args = {
["products"] = TemplateQueryData.Products
}
};
}
}
Which can be called with /products/view to display exactly the same content as calling ProductsPage directly, but instead uses the Routing and the argument populated by the ServiceStack Service, instead of the argument populated in the SharpPagesFeature Arguments.
Using the Request.GetCodePage() extension method is the recommended way to resolve Code Pages as it will automatically inject the current IRequest on Code Pages that implement IRequiresRequest like ServiceStackCodePage's. It's the same as resolving the Code Page from the ISharpPages dependency and injecting the IRequest manually:
public ISharpPages Pages { get; set; }
public object Any(ViewProducts request)
{
var codePage = Pages.GetCodePage("products");
(codePage as IRequiresRequest)?.Request = Request;
return new PageResult(codePage) { ... };
}
Code Pages as Partials
Another area Code Pages are useful are as partials where they're able to escape the normal sandbox of #Script Pages and and use C# to access the existing functionality available within your Web App.
Partial Arguments
You can use an Object literal to pass arguments to Code Page partials (i.e. same as page partials), where each property creates a scoped argument that the Code Page has access to.
We'll show an example of this by building a simple navLinks Code Page partial where we pass in the links we want generated when embed the partial:
{{ 'navLinks' |> partial(
{
links: {
'/docs/model-view-controller': 'MVC',
'/docs/sharp-pages': '#Script Pages',
'/docs/code-pages': 'Sharp Code Pages'
}
})
}}
Which will generate the following links of progressive advanced features, with the link of the current page highlighted:
Using the navLinks implementation below where it can access both the PathInfo PageResult argument and the links argument created when the partial was called:
CodePagePartials.cs
using System.Linq;
using System.Collections.Generic;
using ServiceStack;
using ServiceStack.Script;
namespace SharpScript
{
[Page("navLinks")]
public class NavLinksPartial : SharpCodePage
{
string render(string PathInfo, Dictionary<string, object> links) => $@"
<ul>
{links.Map(entry => $@"<li class='{GetClass(PathInfo, entry.Key)}'>
<a href='{entry.Key}'>{entry.Value}</a>
</li>").Join("")}
</ul>";
string GetClass(string pathInfo, string url) => url == pathInfo ? "active" : "";
}
[Page("customerCard")]
public class CustomerCardPartial : SharpCodePage
{
public ICustomers Customers { get; set; }
string render(string customerId) => renderCustomer(Customers.GetCustomer(customerId));
string renderCustomer(Customer customer) => $@"
<table class='table table-bordered'>
<caption>{customer.CompanyName}</caption>
<thead class='thead-inverse'>
<tr>
<th>Address</th>
<th>Phone</th>
<th>Fax</th>
</tr>
</thead>
<tr>
<td>
{customer.Address}
{customer.City}, {customer.PostalCode}, {customer.Country}
</td>
<td>{customer.Phone}</td>
<td>{customer.Fax}</td>
</tr>
</table>";
}
}
The customerCard partial shows another example where Code Pages are useful where they're able to access any IOC dependency and existing Web App functionality, which can be called with:
{{ 'customerCard' |> partial({ customerId: "ALFKI" }) }}
To render a nice Customer Card snapshot:
Address | Phone | Fax |
---|---|---|
Obere Str. 57 Berlin, 12209, Germany | 030-0074321 | 030-0076545 |
ServiceStack Code Pages
ServiceStackCodePage is a convenience base class that inherits SharpCodePage and enhances it with the same functionality that's available in ServiceStack's Service base class, e.g: access to the current Request context, Gateway, Database, Redis, CacheClient, Users Session, AuthRepository, etc.
So you could rewrite the previous Code Page Partials to inherit ServiceStackCodePage instead where NavLinksPartial can access the IRequest directly without having to request the PathInfo argument and CustomerCardPartial has direct access to the configured database:
using System.Linq;
using System.Collections.Generic;
using ServiceStack;
using ServiceStack.Script;
namespace SharpScript
{
[Page("navLinks")]
public class NavLinksPartial : ServiceStackCodePage
{
string render(Dictionary<string, object> links) => $@"
<ul>
{links.Map(entry => $@"<li class='{GetClass(entry.Key)}'>
<a href='{entry.Key}'>{entry.Value}</a>
</li>").Join("")}
</ul>";
string GetClass(string url) => url == Request.PathInfo ? "active" : "";
}
[Page("customerCard")]
public class CustomerCardPartial : ServiceStackCodePage
{
string render(string customerId) => renderCustomer(Db.SingleById<Customer>(customerId));
string renderCustomer(Customer customer) => $@"
<table class='table table-bordered'>
<caption>{customer.CompanyName}</caption>
<thead class='thead-inverse'>
<tr>
<th>Address</th>
<th>Phone</th>
<th>Fax</th>
</tr>
</thead>
<tr>
<td>
{customer.Address}
{customer.City}, {customer.PostalCode}, {customer.Country}
</td>
<td>{customer.Phone}</td>
<td>{customer.Fax}</td>
</tr>
</table>";
}
}
Since they have access to the same functionality ServiceStack Services do, ServiceStackCodePage's can provide an alternative to using Services in Model View Controller.