How to build an online collaborative text editor using .NET

Published Dec 11, 2017Last updated Feb 17, 2018

Whether it’s real-time audio or video chats, or just collaborating on documents in real-time via Google Docs, there are many times when collaborating online in real-time is a huge time-saver and a necessity to keep up with your productivity.

In this tutorial, I’ll walk you through building a basic collaborative text editor using ASP.NET MVC. We’ll also demonstrate how to easily integrate real-time features using Pusher real-time Web technology.

Sample of the working app

Prerequisites

To follow along you should know the basics of:

  • ASP .NET MVC
  • JavaScript(jQuery)
    Setting up Pusher App
  • Sign up or login to your Pusher account.
  • Create a new Pusher app.

Creating Pusher app

Note your Pusher app details you just created:

app_id  = "*********"
key     = "***********************"
secret  =  "*********************"
cluster = "**"

Creating a new ASP.NET MVC Project

Create a new ASP.NET MVC app using your favorite IDE or editor. In this tutorial, I’ll be using Visual Studio.

Creating an ASP.NET MVC project

Installing Pusher ASP.NET Library

Now install Pusher’s .NET library via the package manager console:

PM> Install-Package PusherServer

If everything goes fine at this point, we are ready to start building our app.

Designing the view

Replace the content in Views/Home/Index.cshtml with the following code:

    @{
        ViewBag.Title = "Collaborative text editor";
    }
     
    <div class="row">
        <div class="col-md-2"> </div>
        <div class="col-md-8">
            <h2>ASP.NET MVC Collaborative text editor</h2>
     
            <div class="form-group">
                <label for="comment">Text Count: <span id="text-count">0</span> </label><br>
                <textarea class="form-control" style="min-width: 100%" rows="10" id="message"></textarea>
            </div>
     
        </div>
        <div class="col-md-2"> </div>
    </div>

Require JavaScript files

To keep things organized and clean, we will write all our JavaScript code in a new file - custom.js.

Now create a new file Scripts/custom.js.

Then update Shared/_Layouts.cshtml with the following code:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>@ViewBag.Title - My ASP.NET Application</title>
        @Styles.Render("~/Content/css")
        @Scripts.Render("~/bundles/modernizr")
     
    </head>
    <body>
       ...
        @Scripts.Render("~/bundles/jquery")
        
        <script src="https://js.pusher.com/4.1/pusher.min.js"></script>
        @Scripts.Render("~/Scripts/custom.js")
        
        @Scripts.Render("~/bundles/bootstrap")
        @RenderSection("scripts", required: false)
      ...
    </body>
    </html>

Here, we added Pusher’s JavaScript library and the custom.js file we just created to Shared/_Layouts.cshtml as can be seen below:

    <script src="https://js.pusher.com/4.1/pusher.min.js"></script>
    @Scripts.Render("~/Scripts/custom.js") 

To uniquely identify every user that opens our application, we’ll generate a unique ID when the page loads and assign it to the user. We’ll also use this unique ID to determine the current user that is modifying the text at any giving time.

Update Scripts/custom.js with the following code:

    // Generates a Unique ID for each given user...
    var uniqueId = Math.random().toString(36).substring(2)
                   + (new Date()).getTime().toString(36);
    
    var timer = 0;

Now, create a new instance of a Pusher object and in doing so connect to Pusher. The application to use is defined by the APP_KEY value that you pass in and that we set up earlier. (You should replace ****************** with your real pusher APP_KEY)

Add the following code to Scripts/custom.js:

    var pusher = new Pusher('******************', { // Replace this with your Pusher APP KEY
        cluster: '**', // Replace this with your Pusher app cluster
        encrypted: true
    });

Next, lets subscribe to a channel and bind that channel to an event.

💡 Channels provide a great way of organizing streams of real-time data. Here, we are
subscribing to the coll-text-editor channel for collaboration (NB: The channel name can be any name you like). Once we are subscribed to a channel, we bind that channel to an event.

💡 Events can be seen as a notification of something happening on your system and are ideal for linking updates to changes in the View. In this case we want to bind to an event which is triggered whenever there are any changes to the textarea.

Add the following code to Scripts/custom.js:

    var channel = pusher.subscribe('coll-text-editor');
    channel.bind('my-event', function(data) {
        // Do not update current text box of the current user typing...
        if (data.user != uniqueId ) {
           
            $("#message").val(data.message);
     
            $("#text-count").text(data.message.length);
        } 
    });

Notice that we are not updating the text input for the current user, using a conditional statement as seen here:

    if (data.user != uniqueId ) {
           
      $("#message").val(data.message);
      $("#text-count").text(data.message.length);
    } 

Now, let’s listen for an event on the textarea input to know when there are any changes to it.
We check for a keyup event. If there is any change, then we send the content of the textarea to our method - /Home/Create - using jQuery’s $.post.

Also add the following code to Scripts/custom.js:

    // Sends the text to the server which in turn is sent to Pusher's server
    $("#message").keyup(function () {
    
        clearTimeout(timer);
     
        $("#text-count").text($("#message").val().length );
     
        timer = setTimeout(function () { $.post("/Home/Create", { message: $("#message").val(), user: uniqueId }); }, 100);
    });

But then, using the keyup event, this means we would be sending a lot of requests. How are we dealing with this? To reduce the number of requests, we made use of the JavaScript setTimeout function.

Also, we are making a POST request which contains the text and user id to a route - /Home/Create - which we’ll create next.

At this point, your Scripts/custom.js file should look exactly like this:

    // Generates a Unique ID for each given user...
    var uniqueId = Math.random().toString(36).substring(2)
                   + (new Date()).getTime().toString(36);
    
    var timer = 0;
     
       //TODO : update with your real pusher app details..
        var pusher = new Pusher('******************', { PUSHER APP KEY
            cluster: '**', //PUSHER APP CLUSTER ID
            encrypted: true
        });
     
    // Subscribes to a channel
     
    var channel = pusher.subscribe('coll-text-editor');
    channel.bind('my-event', function(data) {
        // Do not update current text box of the current user typing...
        if (data.user != uniqueId ) {
           
            $("#message").val(data.message);
     
            $("#text-count").text(data.message.length);
        }
        
    });
     
    // Sends the text to the server which in turn is sent to Pusher's server
    $("#message").keyup(function () {
    
        clearTimeout(timer);
     
        $("#text-count").text($("#message").val().length );
     
        timer = setTimeout(function () { $.post("/Home/Create", { message: $("#message").val(), user: uniqueId }); }, 100);
    });

Creating the Controller for sending of messages to Pusher

This acts as the server for our application while the HTML and JavaScript part is the client. When there are any changes to the text, a POST request is sent to this method. In turn the data sent here is sent to Pusher. So Pusher sends the data back to our application and we act on it by updating the textarea of other users viewing the application.

Open up Controllers/HomeController.cs and update it with the following code:

    [HttpPost]
    public async Task<ActionResult> Create(string message, string user)
    {
        var options = new PusherOptions
        {
            Cluster = "eu",
            Encrypted = true
        };
     
       //TODO: update  with your real pusher app details
        var pusher = new Pusher(
          "******",// app id
          "****************", // app key
          "******************", // app secrete
          options);
     
        var result = await pusher.TriggerAsync(
          "coll-text-editor", // update with a chanel name
          "my-event",
          new { message = message, user = user });
     
        return new HttpStatusCodeResult((int)HttpStatusCode.OK);
    }

Above, we created a Method - Create -, which will only respond to POST requests. When a user updates the text, we send the text and the userid to this method and from here it’s then sent to Pusher. Make sure you update your app id, app key and app secrete with your correct Pusher app details.

Testing the App

Relax and start collaborating! ☺️

Sample of the working app

Conclusion

In this tutorial, we have seen how easy it is to build a real-time collaborative text editor in ASP.NET by leveraging Pusher's technology. Although this is a simple(basic) collaborative text editor, there is no limit to what you can build here. You are just limited by your creativity; we’d be glad to hear of what you will be building.

Discover and read more posts from Onwuka Gideon
get started