Introduction
ASP.NET Core Razor Pages is a relatively recent addition to the ASP.NET Core. It was introduced with .NET Core v2.0, as a lightweight alternative to full-blown MVC. It proposes a much simpler programming model that is pretty similar to the old WebForms, except that it's stateless and free of server-side controls. Instead of writing controllers that return views, a developer's focus would be on creating page views, with code-behind that resembles an MVVM framework. Routing doesn't need to be explicitly configured, but is based on convention of using the file names you give to the pages.
Razor pages is easy to pick up by beginners. It's most appropriate for static sites and other simple, server-side rendered solutions that don't involve too much interactivity. But for applications that do require more sophisticated user interaction, it is often suggested to prefer using JavaScript client-side framework with data served by Web APIs.
But won't it be great to keep the simplicity of Razor Pages while still allowing us to develop rich client-side rendered solutions? The following sections in this article will demonstrate how to use a Razor page to build a client-side rendered form in MVVM style, and where every field configuration, including both client- and server-side validations are defined in a C# class.
For this effort, we will be using dotNetify-Elements component library which I wrote. This library is basically a set of ReactJS components that are customized for integration with .NET C# classes and communicate via SignalR. It is also closely integrated with Rx.NET (System.Reactive
) which promotes a way to write code that's more simple and maintainable.
Reactive Programming
Before we begin writing code, let's have a quick overview first on reactive programming. I have yet to find a definition that can be immediately grasped, but my own take on it is that it's a way of writing code that deals with data stream (like the text that a user is typing on a text field in a browser, going into a property of a C# class instance in the back-end) where instead of data being proactively pushed by the data source (or publisher) to whoever wants it (or subscribers), it's the subscribers that listen to the change in publisher's state and react to it.
This explanation can actually describe any event-driven programming technique, but the important difference lies in the abstractions that the Reactive library provides. They allow us to implement the concept in an expressive, declarative, and asynchronous manner, and take it further by providing a rich toolset to manipulate and transform the data stream, such as mapping, reducing, and combining one or more streams into something else.
For a simple example, consider the relationship between a light switch and a light bulb. If we were to program it, it would typically be like this:
private bool _switch;
public bool Switch
{
get { return _switch; }
set {
_switch = value;
LightBulb = value ? State.On : State.Off;
}
}
public State LightBulb { get; set; }
The Switch
is coupled with the LightBulb
, and the LightBulb
depends on the Switch
to change its own state. What if we add a third state to the LightBulb
, or we want to hook up a second light bulb to the Switch
? You have to revisit the Switch
's implementation and change it accordingly, even though it's something external to the Switch
that changes.
Let's re-examine it after we refactor the code to a reactive approach:
public ReactiveProperty<bool> Switch => new ReactiveProperty<bool>();
public ReactiveProperty<State> LightBulb => new ReactiveProperty<State>();
constructor()
{
Switch
.SubscribedBy(LightBulb, switchValue => switchValue ? State.On : State.Off);
}
Using the new abstractions, we turn the Switch
and LightBulb
into reactive properties that can be subscribed. We make them decoupled from each other, and establish their relationship with an explicit, declarative and chainable command. Adding a second light bulb would just add a new chain in the constructor, and a new third LightBulb
's state would be contained in the functional mapping logic of the SubscribedBy
command.
This is such a simple example that the benefits may appear subtle, but when we extrapolate this onto a much more complex application, this kind of programming has a great potential to make our codebase much more simple, maintainable and scalable.
Reactive Razor Page Step-by-Step
For the following exercise, you will need to install .NET Core v2.1 (or latest) SDK, and I recommend you use Visual Studio Code with a command-line terminal.
Step 1: New Razor Pages Project
Start by creating a new ASP.NET Core Razor Pages Web App from the official template from the command line:
dotnet new razor
It will create a project that has a few pages. We will implement our form in the Index page, so we won't need a lot of these default files. Let's get rid of the following:
- All files in Pages/Shared except _Layout.cshtml.
- All files in Pages except _ViewImports.cshtml, _ViewStart.cshtml, Index.cshtml and Index.cshtml.cs.
- Everything under wwwroot except favicon.ico (we won't need those CSS, images and scripts).
Step 2: Include Scripts to Main Layout
Open Pages/Shared/_Layout.cshtml and replace the entire content with the following:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/dotnetify-elements@0.1.1/dotnetify-elements.css" rel="stylesheet" />
</head>
<body>
@RenderBody()
<script src="https://unpkg.com/react@16.3.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.3.2/umd/react-dom.production.min.js"></script>
<script src=
"https://cdnjs.cloudflare.com/ajax/libs/styled-components/3.3.3/styled-components.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://unpkg.com/dotnetify@3.0.1/dist/signalR-netcore.js"></script>
<script src="https://unpkg.com/dotnetify@3.0.1/dist/dotnetify-hub.js"></script>
<script src="https://unpkg.com/dotnetify@3.0.1/dist/dotnetify-react.min.js"></script>
<script src="https://unpkg.com/dotnetify-elements@0.1.1/lib/dotnetify-elements.bundle.js">
</script>
</body>
</html>
What we did was to clean up the main layout from unnecessary default code, and then add all the CDN scripts required by the dotNetify-Elements
library. As you can see, it uses the Bootstrap 4 stylesheet, and depends on:
- ReactJS - view library to render our client-side form
- Styled-Components - a ReactJS-based CSS-in-JS library for UI styling
- Babel - translates our code that we will write in latest JavaScript syntax to something older browsers can understand
- JQuery - provides common utilities
- SignalR .NET Core - provides the transport layer between the browser and our ASP.NET server.
Step 3: Install dotNetify Server-Side Libraries
Execute the following commands to install the libraries from NuGet:
dotnet add package DotNetify.SignalR
dotnet add package DotNetify.Elements
dotnet restore
Step 4: Configure dotNetify and SignalR
Replace the Startup.cs with the following (replace the namespace with yours):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using DotNetify;
namespace Razor
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddSignalR();
services.AddDotNetify();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseWebSockets();
app.UseSignalR(routes => routes.MapDotNetifyHub());
app.UseDotNetify();
app.UseStaticFiles();
app.UseMvc();
}
}
}
Step 5: Implement Reactive Form C# Class
The form we are implementing is a conference registration form that will collect the following information with validation:
Name
- required Email
- required, must conform to standard email pattern, must not be used in past registration TShirtSize
- either not specified, or one of these: S
, M
, L
, XL
Open Index.cshtml.cs, and replace with the following:
using DotNetify;
using DotNetify.Elements;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
namespace Razor.Pages
{
public class IndexVM : BaseVM
{
private class FormData
{
public string Name { get; set; }
public string Email { get; set; }
public string TShirtSize { get; set; }
}
private List<FormData> _registeredList = new List<FormData>();
public IndexVM()
{
var clearForm = AddInternalProperty<bool>("ClearForm");
AddProperty<string>("Name")
.WithAttribute(new TextFieldAttribute
{
Label = "Name:",
Placeholder = "Enter your name (required)"
})
.WithRequiredValidation()
.SubscribeTo(clearForm.Select(_ => ""));
AddProperty<string>("Email")
.WithAttribute(new TextFieldAttribute
{
Label = "Email:",
Placeholder = "Enter your email address"
})
.WithRequiredValidation()
.WithPatternValidation(Pattern.Email, "Must be a valid email address.")
.WithServerValidation(ValidateEmailNotRegistered, "Email already registered")
.SubscribeTo(clearForm.Select(_ => ""));
AddProperty<string>("TShirtSize")
.WithAttribute(new DropdownListAttribute
{
Label = "T-Shirt Size:",
Placeholder = "Select your T-Shirt size...",
Options = new Dictionary<string, string>
{
{ "", "" },
{ "S", "Small" },
{ "M", "Medium" },
{ "L", "Large" },
{ "XL", "X-Large" }
}.ToArray()
})
.SubscribeTo(clearForm.Select(_ => ""));
AddProperty<FormData>("Register")
.WithAttribute(new { Label = "Register" })
.SubscribedBy(
AddProperty<string>("ServerResponse"), submittedData => Save(submittedData))
.SubscribedBy(clearForm, _ => true);
}
private string Save(FormData data)
{
_registeredList.Add(data);
return $"The name __'{data.Name}'__ with email '{data.Email}' was successfully registered.";
}
private bool ValidateEmailNotRegistered(string email) =>
!_registeredList.Any(x => x.Email == email);
}
}
Let's examine this code. We added three reactive properties to collect Name
, Email
, and TShirtSize
information, and then proceed to define configuration (label
, placeholder
, dropdown
options) and the validations for each property declaratively by using chainable APIs provided by DotNetify.Elements
namespace.
At runtime, the configuration and validations we have defined here will be used by the dotNetify
library to initialize the client-side UI components. Validations like the required and pattern validations are client-side, so when the user completes their input, it be validated right there on the browser. The library conveniently takes care of server-side validations, like the validation to check whether the email has been registered that we execute on the back-end, by sending entered text to the server to be validated, all without requiring you to write your own code.
There will be a Submit button that will be associated with the Register
property. The property will receive the entire information on that button click. We added another property called ServerReponse
to subscribe to the Register
property, and in turn send feedback to the browser to indicate the server has received the information.
The last property is the ClearForm
property, created as internal
property, which means the value will not be sent to the browser. It is subscribed by all three input properties to clear their own field values, which occurs when the ServerResponse
property publishes its value.
Final Step: Implement Reactive Form View
Open Index.cshtml and replace the content with the following:
@page
<div id="Mount" />
<script type="text/babel">
const { Main, Section, Frame, Panel, Alert, Button, Form,
TextField, DropdownList, VMContext } = dotNetifyElements;
const IndexPage = _ => (
<Main css="height: 100vh">
<Section>
<Frame>
<VMContext vm="IndexVM">
<h2>Registration Form</h2>
<Form>
<Panel>
<TextField id="Name" />
<TextField id="Email" />
<DropdownList id="TShirtSize" />
<Panel right>
<Button label="Cancel" cancel secondary />
<Button id="Register" submit />
</Panel>
<Alert id="ServerResponse" />
</Panel>
</Form>
</VMContext>
</Frame>
</Section>
</Main>
);
ReactDOM.render(<IndexPage />, document.getElementById('Mount'));
</script>
We put a div
tag with id
'Mount
', where ReactJS will render the page component (at the last line inside the script
tag). Inside the script
tag, we implemented the page component, which is composed of UI components from the dotNetify-Elements library (every component is documented in the website).
The dotNetify-Elements
components are designed to minimize the need for developers to write JavaScript code. By convention, a component is matched with a C# class property using the id
attribute. When matched, the configuration and validations defined in that C# class will be automatically used to initialize the component. The identification of the C# class itself is through the VMContext
component that specifies the name of the C# class, in our case, it's IndexVM
.
Note that we're using the Babel
library to do runtime compilation of the script. It is easy and convenient, but carries performance penalty. If this poses a concern, it is remedied by setting up a build tool like WebPack to compile the scripts before deployment.
Run the Project
Run the project by entering:
dotnet run
When you go to the address localhost:5000
, you should see the form. Test the validations. If you register the same email twice, the Email
text field should display "Email not registered
" message and will not allow you to submit.
Advanced Examples
Also attached is the source code of more advanced examples on using Razor Pages for a complex, nested form that can open a modal input dialog, and also a live dashboard. Check the documentation in the website for details on the components involved in these examples.
Summary
ASP.NET Core Razor Pages is a new Microsoft's offering for doing web development, although the technology itself isn't new. It's based on the existing MVC Razor View but with notable improvements that aim to make the development of server-rendered web pages simpler and more organized.
For developers that desire to use Razor Pages for a more sophisticated application with rich client-side interaction, we offer integration with dotNetify-Elements
, an open source library of ReactJS components that are designed to work seamlessly with .NET C# classes and use SignalR for the transport layer.
DotNetify-Elements
aims to reduce the complexity associated with modern web application development from .NET developer's perspective. It is well-documented and has rich reatures. Aside from having many useful components; it provides simple layout system to quickly set up page views like this one and even more complex ones that have navigational sidebar; it supports theme and advanced customization. And by virtue of using SignalR, it can provide real-time data push to browsers, such as live monitoring from IoT devices and other real-time visualization.
Further readings about the library: