Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
461 lines (367 sloc) 17.9 KB
<!--
title: Sharp Pages
order: 19
-->
<p>
One of the most popular use-cases for a high-performance and versatile scripting language like #Script is as a server-side
HTML Sharp Pages for .NET Web Applications where it can provide a simpler, cleaner and portable alternative than Razor and
Razor Pages in ASP.NET and ASP.NET Core Web Apps.
</p>
<h3>Sharp Pages in ServiceStack</h3>
<p>
The <em>SharpPagesFeature</em> plugin provides a first-class experience for generating dynamic websites where it's able to
generate complete server-generated websites (like this one) without requiring any additional Controllers or Services.
</p>
<p>
To enable Sharp Pages in ServiceStack you just need to register the <em>SharpPagesFeature</em> plugin:
</p>
{{ 'gfm/sharp-pages/01.md' | githubMarkdown }}
<p>
<em>SharpPagesFeature</em> is a subclass of <em>ScriptContext</em> which defines the context on which all ServiceStack
Sharp Pages are executed within. It provides deep integration within ServiceStack by replacing the ScriptContext's stand-alone
dependencies with ServiceStack AppHost providers, where it:
</p>
<ul>
<li>Configures it to use ServiceStack's <a href="https://docs.servicestack.net/virtual-file-system">Virtual File Sources</a>
allowing Pages to be loaded from any configured VFS Source</li>
<li>Configures it to use ServiceStack's <a href="https://docs.servicestack.net/ioc">Funq IOC Container</a>
so all ServiceStack dependencies are available to Code Pages</li>
<li>Configures it to use ServiceStack's <a href="https://docs.servicestack.net/appsettings">AppSettings</a>
so all AppHost AppSettings are available to Sharp Pages as well</li>
<li>Configures <b>ScanAssemblies</b> to use AppHost Service Assemblies so it auto-registers all Filters in Service .dlls</li>
<li>Registers the <em>ProtectedScripts</em> allowing pages to access richer server-side functionality</li>
<li>Registers the <b>markdown</b> Filter Transformer using ServiceStack's built-in MarkdownDeep implementation</li>
<li>Makes the <em>ServiceStackCodePage</em> subclass available so Code Pages has access to same functionality as Services</li>
<li>Registers a Request Handler which enables all requests <b>.html</b> pages to be handled by Sharp Pages</li>
</ul>
<p>If preferred, you can change which <b>.html</b> extension gets handled by Sharp Pages with:</p>
{{ 'gfm/sharp-pages/02.md' | githubMarkdown }}
<h4 id="runs-everywhere">Runs Everywhere</h4>
<p>
The beauty of #Script working natively with ServiceStack is that it runs everywhere ServiceStack does
which is in all major .NET Server Platforms. That is, your same #Script-based Web Application is able to use
the same #Script implementation, "flavour" and feature-set and is portable across whichever platform you choose to host it on:
</p>
<ul>
<li><b>Windows, OSX or Linux</b>
<ul>
<li><b>.NET Framework or Mono</b>
<ul>
<li><a href="https://github.com/ServiceStackApps/LiveDemos#live-servicestack-demos">Any ASP.NET host</a></li>
<li><a href="https://docs.servicestack.net/self-hosting">Stand-alone, Self-Hosted HttpListener</a></li>
<li><a href="https://github.com/ServiceStack/ServiceStack.Gap#self-hosting-console-app">Entire App ILMerged into a single cross-platform App.exe</a></li>
</ul>
</li>
<li><b>.NET Core</b>
<ul>
<li><a href="https://github.com/NetCoreApps/LiveDemos#servicestack-net-core-live-demos">Web App or SelfHost</a></li>
</ul>
</li>
</ul>
</li>
<li><b>Windows</b>
<ul>
<li><a href="https://docs.servicestack.net/templates-windows-service">Stand-alone Windows Service</a></li>
<li><a href="https://github.com/ServiceStack/ServiceStack.Gap#winforms-with-chromium-embedded-framework">Hosted inside WinForms with Chromium Embedded Framework</a></li>
<li><a href="https://github.com/ServiceStackApps/HelloServiceFabric">Windows and Azure Service Fabric</a></li>
</ul>
</li>
<li><b>OSX</b>
<ul>
<li><a href="https://github.com/ServiceStack/ServiceStack.Gap#mac-osx-cocoa-app-with-xmarainmac">Hosted inside Mac OSX Cocoa App with Xmarain.Mac</a></li>
</ul>
</li>
</ul>
<p>
Once registered, <em>SharpPagesFeature</em> gives all your <b>.html</b> pages scripting super powers where sections can be
compartmentalized and any duplicated content can now be extracted into reusable partials, metadata can be added to the top of
each page and its page navigation dynamically generated, contents of files and urls can be embedded directly and otherwise
static pages can come alive with access to <a href="/docs/default-scripts">Default Scripts</a>.
</p>
<h2 id="content-pages">Content Pages</h2>
<p>
There are a number of different ways you can use <em>#Script</em> to render dynamic pages, requests that calls and renders
<em>#Script</em> <b>.html</b> pages directly are called <b>"Content Pages"</b> which don't use any Services or Controllers
and can be called using a number of different styles and calling conventions:
</p>
<h3 id="page-based-routing">Page Based Routing</h3>
<p>
Any <em>.html</em> page available from your AppHost's configured <a href="https://docs.servicestack.net/virtual-file-system">Virtual File Sources</a>
can be called directly, typically this would mean the File System which in a .NET Core Web App starts from the <b>WebRootPath</b>
(e.g <em>/wwwroot</em>) so a request to <em>/docs/sharp-pages</em> goes through all configured <em>VirtualFileSources</em> to find the first
match, which for this website is the file
<a href="https://github.com/ServiceStack/sharpscript/blob/master/src/wwwroot/docs/sharp-pages.html">/src/wwwroot/docs/sharp-pages.html</a>.
</p>
<h4 id="pretty-urls-by-default">Pretty URLs by default</h4>
<p>
Essentially Sharp Pages embraces conventional page-based routing which enables <b>pretty urls</b> inferred from the pages file and directory names
where each page can be requested with or without its <b>.html</b> extension:
</p>
<table class="table">
<thead>
<tr>
<th>path</th>
<th>page</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://blog.web-app.io/db">/db</a></td>
<td>&nbsp;</td>
</tr>
<tr>
<td><a href="http://blog.web-app.io/db.html">/db.html</a></td>
<td><a href="https://github.com/NetCoreWebApps/Blog/blob/master/db.html">/db.html</a></td>
</tr>
<tr>
<td><a href="http://blog.web-app.io/posts/new">/posts/new</a></td>
<td>&nbsp;</td>
</tr>
<tr>
<td><a href="http://blog.web-app.io/posts/new.html">/posts/new.html</a></td>
<td><a href="https://github.com/NetCoreWebApps/Blog/blob/master/posts/new.html">/posts/new.html</a></td>
</tr>
</tbody>
</table>
<p>
The default route <em>/</em> maps to the <em>index.html</em> in the directory if it exists, e.g:
</p>
<table class="table">
<thead>
<tr>
<th>path</th>
<th>page</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://blog.web-app.io/">/</a></td>
<td><a href="https://github.com/NetCoreWebApps/Blog/blob/master/index.html">/index.html</a></td>
</tr>
<tr>
<td><a href="http://blog.web-app.io/index.html">/index.html</a></td>
<td><a href="https://github.com/NetCoreWebApps/Blog/blob/master/index.html">/index.html</a></td>
</tr>
</tbody>
</table>
<h3 id="dynamic-page-routes">Dynamic Page Routes</h3>
<p>
In addition to these static conventions, Sharp Pages now supports Nuxt-like
<a href="https://nuxtjs.org/guide/routing#dynamic-routes">Dynamic Routes</a>
where any <b>file</b> or <b>directory</b> names prefixed with an <b>_underscore</b> enables a wildcard path which assigns the matching path component
to the arguments name:
</p>
<table class="table">
<thead>
<tr>
<th>path</th>
<th>page</th>
<th>arguments</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://blog.web-app.io/">/ServiceStack</a></td>
<td><a href="https://github.com/NetCoreWebApps/Blog/blob/master/_user/index.html">/_user/index.html</a></td>
<td>user=ServiceStack</td>
</tr>
<tr>
<td><a href="http://blog.web-app.io/posts/markdown-example">/posts/markdown-example</a></td>
<td><a href="https://github.com/NetCoreWebApps/Blog/blob/master/posts/_slug/index.html">/posts/_slug/index.html</a></td>
<td>slug=markdown-example</td>
</tr>
<tr>
<td><a href="http://blog.web-app.io/posts/markdown-example/edit">/posts/markdown-example/edit</a></td>
<td><a href="https://github.com/NetCoreWebApps/Blog/blob/master/posts/_slug/edit.html">/posts/_slug/edit.html</a></td>
<td>slug=markdown-example</td>
</tr>
</tbody>
</table>
<h4>Layout and partial recommended naming conventions</h4>
<p>
The <b>_underscore</b> prefix for declaring wildcard pages is also what is used to declare "hidden" pages, to distinguish them from hidden
partials and layouts, the recommendation is for them to include <em>layout</em> and <em>partial</em> their name, e,g:
</p>
<ul>
<li>_layout.html</li>
<li>_alt-layout.html</li>
<li>_menu-partial.html</li>
</ul>
<blockquote>
Pages with <em>layout</em> or <em>partial</em> in their name remain hidden and are ignored in wildcard path resolution.
</blockquote>
<p>
If following the recommended <em>_{name}-partial.html</em> naming convention, you'll be able to reference them using just their name:
</p>
{{#raw}}
<pre>
{{ 'menu' | partial }} // Equivalent to:
{{ '_menu-partial' | partial }}
</pre>
{{/raw}}
<h2 id="view-pages">View Pages</h2>
<p>
"View Pages" are pages that are rendered <b>after a Service is executed</b>, where it's typically used to provide the "HTML UI"
for the Service's <b>Response DTO</b> where it's populated in the <em>Model</em> page argument
as done in <a href="http://razor.netcore.io/#smart-views">Razor ViewPages</a>.
</p>
<p>
View Pages can be in any nested folder within the <em>/Views</em> folder but all page names within the <em>/Views</em> folder need to be unique.
The name should use the format using the format <em>{PageName}.html</em> where <em>PageName</em> can be either:
</p>
<ul>
<li><b>Request DTO</b> Name (e.g. <em>GetContact</em>)</li>
<li><b>Response DTO</b> Name (e.g. <em>GetContactResponse</em>)</li>
</ul>
<p>
There are a number of other ways to specify which <b>View</b> you want to render:
</p>
<h3 id="default-view">Specify the Services DefaultView</h3>
<p>
You can specify which view all Services should use to render their responses by using the <em>[DefaultView]</em> Request Filter Attribute:
</p>
{{ 'gfm/sharp-pages/12.md' | githubMarkdown }}
<h3 id="viewpage-htmlresult">Specify View with custom <em>HttpResult</em></h3>
<p>
Just like ServiceStack.Razor, you can specify to use different Views or Layouts by returning a
decorated response in custom <em>HttpResult</em> with the <em>View</em> or <em>Template</em> you want the Service rendered in , e.g:
</p>
{{ 'gfm/sharp-pages/09.md' | githubMarkdown }}
<h3 id="custom-pageresult">Return Custom PageResult</h3>
<p>
For maximum flexibility to control how views are rendered you can return a custom <em>PageResult</em> using
<em>Request.GetPage()</em> or <em>Request.GetCodePage()</em> extension methods as seen in
<a href="/docs/model-view-controller">Model View Controller</a>:
</p>
{{ 'gfm/model-view-controller/02.md' | githubMarkdown }}
<h3 id="ClientCanSwapTemplates">Allow Views to be specified on the QueryString</h3>
<p>
You can use the <em>[ClientCanSwapTemplates]</em> Request Filter attribute to let the View and Template by specified on the QueryString,
e.g: <em>?View=CustomPage&Template=_custom-layout</em>
</p>
{{ 'gfm/sharp-pages/13.md' | githubMarkdown }}
<p>
Additional examples of dynamically specifying the View and Template are available in
<a href="https://github.com/ServiceStack/ServiceStack/blob/master/tests/ServiceStack.WebHost.Endpoints.Tests/ScriptTests/SharpViewsTests.cs">SharpViewsTests.cs</a>.
</p>
<h4 id="cascading-layouts">Cascading Layouts</h4>
<p>
One difference from Razor is that it uses a cascading <em>_layout.html</em> instead of <em>/Views/Shared/_Layout.cshtml</em>.
</p>
<p>So if your view page was in:</p>
<div class="gfm">
<pre>
/Views/dir/MyRequest.html
</pre>
</div>
<p>It will use the closest `_layout.html` it can find starting from:</p>
<div class="gfm">
<pre>
/Views/dir/_layout.html
/Views/_layout.html
/_layout.html
</pre>
</div>
<h4 id="layout-selection">Layout Selection</h4>
<p>
Unless it's a complete HTML Page (e.g. starts with html or HTML5 tag) the page gets rendered using the closest <em>_layout.html</em>
page it can find starting from the directory where the page is located, traversing all the way up until it reaches the root directory.
Which for this page uses the
<a href="https://github.com/ServiceStack/sharpscript/blob/master/src/wwwroot/_layout.html">/src/wwwroot/_layout.html</a> template
in the WebRoot directory, which as it's in the root directory, is the fallback Layout for all .html pages.
</p>
<p>
Pages can change the layout they use by either adding their own <em>_layout.html</em> page in their sub directory or specifying
a different layout in their pages metadata header, e.g:
</p>
{{ 'gfm/sharp-pages/03.md' | githubMarkdown }}
<p>
Where it instead embed the page using the closest <b>mobile-layout.html</b> it can find, starting from the Page's directory.
If your pages are instead embedded in the different folder you can request it directly from the root dir:
</p>
{{ 'gfm/sharp-pages/04.md' | githubMarkdown }}
<h4>Request Variables</h4>
<p>
The QueryString and FORM variables sent to the page are available as arguments within the page using the
<em>form</em> and <em>query</em> (or its shorter <em>qs</em> alias) collections, so a request like
<a href="/docs/sharp-pages?id=1">/docs/sharp-pages?id=1</a>
can access the <b>id</b> param with <em>{{ pass: qs.id }}</em>. The combined <em>{{pass: 'id' | formQuery }}</em>
method enables the popular use-case of checking for the param in POST <em>FormData</em> before falling back to
the <em>QueryString</em>. Use <em>{{pass: 'ids' | formQueryValues }}</em> for accessing multiple values sent by multiple checkboxes
or multiple select inputs. The <em>{{pass: 'id' | httpParam }}</em> method searches all Request params including HTTP Headers, QueryString,
FormData, Cookies and Request.Items.
</p>
<p>
To help with generating navigation, the following Request Variables are also available:
</p>
<ul>
<li><em>{{ pass: Verb }}</em> evaluates to <b>{{ Verb }}</b></li>
<li><em>{{ pass: AbsoluteUri }}</em> evaluates to <b>{{ AbsoluteUri }}</b></li>
<li><em>{{ pass: RawUrl }}</em> evaluates to <b>{{ RawUrl }}</b></li>
<li><em>{{ pass: PathInfo }}</em> evaluates to <b>{{ PathInfo }}</b></li>
</ul>
<p>
You can use <em>{{ pass: PathInfo }}</em> to easily highlight the active link in a links menu as done in
<a href="https://github.com/ServiceStack/sharpscript/blob/master/src/wwwroot/sidebar.html">sidebar.html</a>:
</p>
{{ 'gfm/sharp-pages/05.md' | githubMarkdown }}
<h3 id="init-pages">Init Pages</h3>
<p>
Just as how <em>Global.asax.cs</em> can be used to run Startup initialization logic in ASP.NET Web Applications and
<em>Startup.cs</em> in .NET Core Apps, you can now add a <em>/_init.html</em> page for any script logic that's only executed once on Startup.
</p>
<p>
This is used in the Blog Web App's <a href="https://github.com/NetCoreWebApps/Blog/blob/master/_init.html">_init.html</a>
where it will create a new <em>blog.sqlite</em> database if it doesn't exist seeded with the
<em>UserInfo</em> and <em>Posts</em> Tables and initial data, e.g:
</p>
{{ 'gfm/sharp-pages/10.md' | githubMarkdown }}
<h4>Ignoring Pages</h4>
<p>
You can ignore #Script from evaluating static <b>.html</b> files with the following page arguments:
</p>
{{ 'gfm/sharp-pages/06.md' | githubMarkdown }}
{{ 'gfm/sharp-pages/07.md' | githubMarkdown }}
{{ 'gfm/sharp-pages/08.md' | githubMarkdown }}
<blockquote>
Complete <b>.html</b> pages starting with <em>&lt;!DOCTYPE HTML&gt;</em> or <em>&lt;html</em> have their layouts ignored by default.
</blockquote>
<h3 id="templates-admin-service">Admin Service</h3>
<p>
The new Admin Service lets you run admin actions against a running instance which by default is only accessible to <b>Admin</b>
users and can be called with:
</p>
<pre><code>/script/admin</code></pre>
<p>
Which will display the available actions which are currently only:
</p>
<ul>
<li><em>invalidateAllCaches</em> - Invalidate all caches and force pages to check if they've been modified on next request</li>
<li><em>RunInitPage</em> - Runs the Init page again</li>
</ul>
<h4>Zero downtime deployments</h4>
<p>
These actions are useful after an xcopy/rsync deployment to enable zero downtime deployments by getting a running instance to invalidate all
internal caches and force existing pages to check if it has been modified, the next time their called.
</p>
<p>
Actions can be invoked in the format with:
</p>
<pre><code>/script/admin/{Actions}</code></pre>
<p>
Which can be used to call 1 or more actions:
</p>
<pre><code>/script/admin/invalidateAllCaches
/script/admin/invalidateAllCaches,RunInitPage</code></pre>
<p>
By default it's only available to be called by **Admin** Users (or <a href="https://docs.servicestack.net/debugging#authsecret">AuthSecret</a>)
but can be changed with:
</p>
{{ 'gfm/sharp-pages/11.md' | githubMarkdown }}
<h3>ServiceStack Scripts</h3>
<p>
Methods for integrating with ServiceStack are available in
<a href="/docs/servicestack-scripts">ServiceStack Scripts</a> and <a href="/docs/servicestack-scripts#info-scripts">Info Scripts</a> both
of which are pre-registered when registering the <em>SharpPagesFeature</em> Plugin.
</p>
{{ "doc-links" | partial({ order }) }}
You can’t perform that action at this time.