Click here to Skip to main content
15,885,757 members
Articles / Web Development / ASP.NET / ASP.NET Core

Turn Your Razor Page Into Reactive Form

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
1 Aug 2018Apache9 min read 20.6K   390   13   3
How to create a powerful reactive form on an ASP.NET Core Razor Page in MVVM style, with field configuration and validations written in C# and communicating with SignalR.

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:

C#
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:

C#
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:

MC++
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:

HTML
<!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:

MC++
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):

C#
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:

C#
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:

JavaScript
@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.

Image 1

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:

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
United States United States
Full-stack s/w engineer, open source author.

Comments and Discussions

 
QuestionCan't work out if this is crazy or cool Pin
Sacha Barber31-Jul-18 20:28
Sacha Barber31-Jul-18 20:28 
Clearly a lot of work has gone into your library

But essentially you are using signalr to write a change back to server side viewmodel on each client side field update right

So if form has 100 fields you would write back 100 unique times (obviously not at same time as they can only change one at a time)

I'm struggling to see what this offers over regular app where I just send the entire json object that represents the viewmodel back to server side and process it in one go

Btw: I did years of wpf and wrote my own mvvm framework so I know about mvvm

AnswerRe: Can't work out if this is crazy or cool Pin
dsuryd1-Aug-18 4:04
dsuryd1-Aug-18 4:04 
GeneralRe: Can't work out if this is crazy or cool Pin
Sacha Barber1-Aug-18 19:37
Sacha Barber1-Aug-18 19:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.