Click here to Skip to main content
Click here to Skip to main content

Frontend design for ASP.NET web applications

, 3 Sep 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
Frontend design for ASP.NET web applications

Introduction

From time to time in ASP.NET web applications you have to deal with frontend development. In some applications there is just a number of javascript functions on ASPX page but in other applications there is a lot of logic in javascript code. After working with different web applications having different design (one page App, common ASP.NET WebForms application with lots of pages) I decided to try to organize my client side code better. In this article I would like to explain my suggestions on organizing client-side code and I would like to hear your thoughts about it.

Problems definition.

Let’s take a look on what we usually have in our ASP.NET WebForms applications. Base bricks for standard ASP.NET WebForms app are ASPX pages or ASCX controls. Usually we have lots of such items in any standard project. Each page or control might have some client side logic with some jquery actions like get data/change data/validate data and etc.

Usually this kind of code being added to the page just as plain javascript functions:

    <script type="text/javascript">
        function SomeFunction() {
            // do some staff
        }
    </script>

Sometimes these functions are added to javascript files and attached in ASPX page header section.

Also it is quite common to bind client side event handlers directly in aspx markup using something like this:

<asp:Button runat="server" OnClientClick="SomeFunction()"/>

We also usually have several javascript files with code which might be dependent on each other and we have to manually attach those scripts in head section of the page with valid order.

As a result we have lots of javascript code added to Global namespace without any module system or a possibility to reuse existing code appropriately. We also have tight coupling between aspx markup and client side code both by control id's and function names. It is almost impossible in such situation to use such features as html templating.

How to resolve

In article below I would try to explain my suggestions on how to organize your client-side code to achieve better modularity and maintainabiltity.

For each page in my ASP.NET WebForms application I would like to have isolated javascript module which I would call controller. Controller should encapsulate all client-side functionality for this specific page. This module will be isolated part of application which would have all required references declared and automatically loaded + it might be unit tested outside of main application flow using any javascript testing framework.

Requirejs and project infrastructure

For module declaration I decided to use requirejs. This library allow us to declare references between scripts and automatically load it.

I use the following folder structure in my ASP.NET WebForms web applications:

js - root folder for all frontend development staff.

js/lib - folder to store 3rd party libraries like jquery and requirejs

js/app - to store application specific code

js/app/common - common modules which might be used in different parts of application

js/app/controller - folder to store page/control specific controllers

To make requirejs work create js/config.js file with the following content:

require.config({
    //By default load any module IDs from js/lib
    baseUrl: 'js/lib',

    paths: {
        app: '../app',
        jquery: 'jquery-2.1.1'
    }
});

Now let’s take a look on Main.Master page markup:

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Main.master.cs" Inherits="WebAppTemplate.Main" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="js/lib/require.js"></script>
    <script src="js/config.js"></script>
    <asp:ContentPlaceHolder ID="HeadContent" runat="server" />
    <script>
        require(["domReady!"],
            function (domReady) {
            }
        );
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <asp:ContentPlaceHolder ID="MainContent" runat="server" />
    </form>
</body>
</html>

In head section we attached require.js library to make requirejs work. Next attached file is config.js which configure requirejs library. Next line is setting content place holder to allow content pages to attach other scripts. Last script section has require statement with domReady.js script loading. Inside the function we can load any modules or do other client-side javascript magic. By using this we just make sure that our code run after DOM is loaded.

In body section we just declare content place holder for content page.

Now we are ready to implement our page-specific requirejs modules.

Used 3rd party components:

  1. Jquery-2.1.1 - http://jquery.com/
  1. Require.js - http://requirejs.org/

          Popular library which allow to write modular javascript code.

  1. domReady.js - Module which allow to determine when DOM is loaded without attaching jquery. This can be replaced with jquery ready function but just as small performance boost I use domReady.js
  1. class.js – simple javascript inheritance implementation by John Resig. I slightly changed implementation to be able to use “base” word to call base functions instead of using “super”. http://ejohn.org/blog/simple-javascript-inheritance/
  1. pubsub.js – simple observer pattern implementation. https://gist.github.com/fatihacet/1290216

Goals to achieve

  1. Stop global namespace pollution using javascript modules.
  2. Enforce code reuse by introducing inheritance and modularity in javascript code
  3. Encapsulate page-specific javascript to isolated modules
  4. Be able to declaratively assign page modules to specific pages
  5. Control dependencies between modules using declarative syntax;
  6. Be able to do common tasks in page-specific modules:
    • Find page-specific controls and manipulate with them
    • Communicate between modules using observer (pubsub) pattern
    • Be able to communicate with server using both submit and ajax
  7. Be able to use both Single Page Application approach and Multi-Page Application MPV approach

Page-Specific modules: step-by-step

Let’s think about what we would like to have in modules.

I would like to have the following features:

  • Possibility to interact with DOM - can be covered by using jquery in modules when needed
  • Possibility to interact between modules - if we have a number of user controls on one page and each user control have its own module we need a way on how to communicate between modules. Simple observer pattern implementation might help here (pubsub).
  • Possibility to reuse code via inheritance - this can be debatable. We can use mixins to reuse code. We can use prototype configuration to reuse code. We can use composition to reuse code. But using inheritance for javascript modules is really helpful for .NET basis developers.

Let’s start to achieve these goals one by one.

If we want to use inheritance mechanics we should declare start point for our inheritance chain. For this purpose I use BaseModule.js module.

Code is simple:

define([
    "class"
],
function(Class) {
// Set base inheritance class. Use extend() method in heirs to continue inheritance chain
    return this.Class.extend({
        // Constructor
        init: function() {
            // Default values can be set in this method
        }
    });
});

This is just simple module which should be starting point for us. It states that each module will have “extend” function from John Resig’s inheritance implementation. Plus we have empty constructor declaration (init method). Comments will help us to understand how to continue inheritance chain. This class can be used (and actually should be used) by any module in our system (maybe except modules which contains of constant values - they doesn’t need inheritance feature at all).

As a next step I would like to have BaseController class which will be starting point of inheritance chain for all page specific controllers. Code might look like this:

define([
    "app/common/BaseModule"
],
function (BaseModule) {
    return BaseModule.extend({
        // Entry point
        start: function() {
            this._loadControls();
            this._setEventHandlers();
        },

        // This method should be used to load page-specific controls and set references to them in controller
        _loadControls: function() {
        },

        // This method should be used to attach event handlers to controls
        _setEventHandlers: function() {
        }
    });
});

This is example of how I would like my controller to behave. Only assumption here is that “start” method should be called after DOM is loaded (this should be added to start method comments by the way). I split controller behavior into 2 phases – loadControls and setEventHandlers.  LoadControls phase should be used to select any DOM elements and set internal controller fields. SetEventHandlers phase should be used to attach to any DOM events or pubsub events. We can do all this staff in “start” function. But I found this split useful.

To be able to use this controller we should call require function in Default.aspx, instantiate new controller and call start method. And this will work as expected. The only disadvantage here is that for each page/control we have to write the same code to instantiate and execute controller. I would like to have declarative way of using controllers. To be able to achieve that I implemented ControllerLoader class which use data-controller attribute to determine the name of controller and dynamically load it using requirejs.

Create the following javascript file:

js\app\common\ControllerLoader.js

Code should be the following:

define([
    "app/common/BaseModule",
    "jquery"
],
function(BaseModule, $) {
    return BaseModule.extend({
        loadDeclaredControllers: function() {
            var controllers = $("[data-controller]");

            for(var i = 0; i < controllers.length; i++) {
                var containerDiv = $(controllers[i]);
                this.loadController(containerDiv.data("controller"), containerDiv);
            }
        },

        loadController: function(controllerName, containerDiv) {
            // Use requirejs to load controller class
            require([controllerName], function(ControllerModule) {
                // Call base module constructor and init() method
                var controller = new ControllerModule();

                // Set container to be able to determine context
                if (containerDiv) {
                    controller.container = containerDiv;
                }

                // Call default entry point for controller if defined
                if (controller.start) {
                    controller.start();
                }
            });
        }
    });
});

As you can see I implemented 2 methods. “loadController” receive the name of controller and reference to containerDiv (optional). The purpose of this method is to instantiate controller and call start method if it does exists. “loadDeclaredControllers” method trying to find tags with “data-controller” attribute and for each found tag it read controller name and execute loadController method to instantiate new controller.

To be able to use that code we should have the following markup in head section of our Main.Master page:

<script src="js/lib/require.js"></script>
<script src="js/config.js"></script>
<asp:ContentPlaceHolder ID="HeadContent" runat="server" />
<script>
    require(["domReady!", "app/common/ControllerLoader"],
        function (domReady, controllerLoader) {
            new controllerLoader().loadDeclaredControllers();
        }
    );
</script>

Now when DOM loaded new controller loader will be instantiated and loadDeclaredControllers function will load all declared controllers.

Controllers should be declared as following:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="PersonProcessed.ascx.cs" Inherits="WebAppTemplate.PersonProcessed" %>

<div data-controller="app/controller/PersonProcessedController">
    Processed Person List <br />
    <ul id="personList"></ul>
</div>

Another thing which we should cover is ASP.NET WebForms controls client ID’s. As you might know, ASP.NET controls use NamingContainer to generate client ID’s. That might be an issue while using jquery to select controls from DOM in controllers because you don’t know final control ID. To handle this I added the following function in my BaseController.js class:

// Helper method to find controls using jQuery with controller container context
getByID: function (id, context) {
    // Set search context if applicable
    if (!context && this.container) {
        context = this.container;
    }

    // Basic jQuery selector
    var el = $("#" + id, context);

    // If unable to find element - lets try to check ASP.NET naming container prefix
    if (el.length < 1) {
        el = $("[id$=_" + id + "]", context);
    }

    return el;
}

"getByID" method will try to find dom element using common jquery selector but if it will be unable to, we will try to use pattern “_ClientID” to find our control. I also decided to use container to specify context of control search.

Let’s check everything in action

Now we are ready to see everything in action.

As example let’s try to implement the following funny behavior.

We have 2 user controls. First one is to enter first name and last name of person. Process Person button should also be there to execute processing. On the second user control we want to have Processed Person List.

Behavior should be the following:

  1. User enter first name and last name
  2. Click on Process Person -> Ajax postback go to server
  3. Some magic server-side processing happens and result is sent back to client via JSON response.
  4. Processed Person List is update with first name and last name.
  5. Controls in first user control refreshed.

This use case is kind of stupid but it allow us to demonstrate all features we want to have.

Let’s start from creating two user controls.

PersonEdit.ascx:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="PersonEdit.ascx.cs" Inherits="WebAppTemplate.PersonEdit" %>

<div data-controller="app/controller/PersonEditController">
    First Name: <asp:TextBox runat="server" ID="txtFirstName"></asp:TextBox>
    Last Name: <asp:TextBox runat="server" ID="txtLastName"></asp:TextBox>
    <button id="btnCheck" type="button">Process Person</button> <br />
</div>

PersonProcessed.ascx:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="PersonProcessed.ascx.cs" Inherits="WebAppTemplate.PersonProcessed" %>

<div data-controller="app/controller/PersonProcessedController">
    Processed Person List <br />
    <ul id="personList"></ul>
</div>

Markup is ready. Now we need to create corresponding controllers. PersonEdit controller should react on click event and call ajax postback to server with entered first name/last name.

define([
    "app/controller/BaseController",
    "pubsub",
    "app/common/topic"
],
function(BaseController, ps, topic) {
    return BaseController.extend({
        txtFirstName: null,
        txtLastName: null,
        btnCheck: null,

        _loadControls: function() {
            this.base();

            this.txtFirstName = this.getByID("txtFirstName");
            this.txtLastName = this.getByID("txtLastName");
            this.btnCheck = this.getByID("btnCheck");
        },

        _setEventHandlers: function() {
            this.base();

            this.btnCheck.on("click", this._onBtnCheckClick.bind(this));

            ps.subscribe(topic.Person_Processed, this._onPersonProcessed.bind(this));
        },

        _onBtnCheckClick: function() {
            ps.publish(topic.Person_Ready_To_Process, { firstName: this.txtFirstName.val(), lastName: this.txtLastName.val() });
        },

        _onPersonProcessed: function (event, args) {
            this.txtFirstName.val("");
            this.txtLastName.val("");

            alert("Person " + args.person + " was successfully processed!");
        }
    });
});

Let’s take a look on what we have here. In loadControls method we selecting controls from DOM. In setEventHandlers we attaching to btnCheck click event. Pay attention to usage of “bind(this)” function. This call allow us to have controller instance in this when handler will be executed. We also subscribed on Person_Processed topic (constant for that must be declared in Topic.js file). In click event handler we get firstName/lastName values from controls and publish Person_Ready_To_Process topic (to execute some logic from second controller). onPersonProcessed handler should be executed after person will be processed on server. In this handler we set values of controls to string.empty and show message box with results.

Let’s take a look now on our PersonProcessedController.js.

define([
    "app/controller/BaseController",
    "pubsub",
    "app/common/topic"
],
function(BaseController, ps, topic) {
    return BaseController.extend({

        ulPerson: null,

        _loadControls: function () {
            this.base();

            this.ulPerson = this.getByID("personList");
        },

        _setEventHandlers: function() {
            this.base();

            ps.subscribe(topic.Person_Ready_To_Process, this._onPersonAdded.bind(this));
        },

        _onPersonAdded: function(event, args) {
            $.ajax({
                url: '/default.aspx',
                dataType:'json',
                data: { command: "processPerson", firstName: args.firstName, lastName: args.lastName },
                success: this._onPersonProcessed.bind(this),
                error: function (xml, status, error) {
                    // do something if there was an error
                    alert(error);
                }
            });
        },

        _onPersonProcessed: function (data) {
            var person = data.FirstName + ' ' + data.LastName;

            this.ulPerson.append($("<li>").text(person));

            ps.publish(topic.Person_Processed, { person: person });
        }
    });
});

In setEventHandlers method we subscribe to Person_Ready_To_Process topic. In onPersonAdded handler we send ajax postback to server. We assume that server will process our person and return us results. Again, pay attention to this._onPersonProcessed.bind(this) syntax. It allow us to have controller instance in this when event handler will be executed. In onPersonProcessed handler we add processed person to the list and publish event which says to the system that person was just processed. This event will be used in PersonEdit controller to update controls and show result message.

On server side I get data from request parameters and write required response. I will not focus on this because this is not main reason for this article. This technique can be used in development of SPA (Single Page Applications). We can also call WCF services for the same way of processing ajax requests.

Possible improvements

I think of the following possible improvements:

  1. Use something like knockout.js for MVVM way of DOM manipulation. It will improve further overall design.
  2. Thinking of how to implement MVP pattern on server side for ASP.NET WebForms using this approach. The only issue here is Ajax requests handling. Everything else lines up to this design perfectly.
  3. Use some javascript unit testing framework to implement unit tests.

Full source code is attached to the article. Please take a look on it and provide your comments. I would like to know if you think that this approach is really useful or not, in which way it can be improved.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

_yaRus_
Team Leader
Russian Federation Russian Federation
Software development Team Lead with 8 years .NET Web development expirience.

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150326.1 | Last Updated 3 Sep 2014
Article Copyright 2014 by _yaRus_
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid