App Modernization - Part #2: Migrate underscore.js templates and JQuery data loading

Published: 5/24/2023 8:00:00 AM

This article is the second part of the series:

 

 

For this example, we will return to our fictional customer internal e-shop pages written in ASP.NET Web Forms. We will migrate an ASPX page that uses jquery and underscore to load and display data.

Legacy solution

This example needs a bit of explanation beforehand. There is a page Products.aspx that displays a list of products. When the user clicks at the product row, a dialog with additional information opens. The data in the product table as well as data in the detail dialog are loaded using JQuery. The page which provides the data for the requests from Products.aspx is also an ASPX page ProductsService.aspx. The service page handles the request and displays JSON as the content of the page. The architecture of the legacy solution is as follows:

-----------------                       ------------------------
| Products.aspx | ------action:list---> | ProductsService.aspx |
----------------- <-------JSON--------- ------------------------
                |                       |
                | ----action:detail---> |
                | <-------JSON--------- |

The files in our project structure we need to look at are these:

Model
  Product.cs
Facades
  ProductFacade.cs
Content
  products.js
Pages
  Products.aspx
    Products.aspx.cs
  ProductsService.aspx
    ProductsService.aspx.cs

Products.aspx:

<form id="ProductsForm" runat="server">
    <script type="text/template" id="product-template">
        <tr>
            <td>
                <input type="hidden" value="{{ product.Id }}"/>
                {{ product.Code }}
            </td>
            <td>{{ product.Name }}</td>
            <td>{{ product.Price }} EUR</td>
        </tr>
    </script>

    <script type="text/template" id="product-dialog-template">
        <div class="product-dialog-template">
            <h3>{{ product.Name }}</h3>
            <p>
                <span>Code:</span> {{ product.Code }}
            </p>
            <p>
                <span>Price:</span> {{ product.Price }} EUR
            </p>
            <p>
                <span>Description:</span> {{ product.Description }}
            </p>
        </div>
    </script>

    <div id="product-dialog"></div>

    <table id="products-table">
        <thead>
            <tr>
                <th>Code</th>
                <th>Name</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>
</form>

products.js:

$(function () {
    _.templateSettings = {
        interpolate: /\{\{(.+?)\}\}/g
    };

    const productTableTemplate = _.template($('#product-template').html());
    const dialogTemplate = _.template($('#product-dialog-template').html());

    const $productDialog = $('#product-dialog');
    $productDialog.dialog({
        autoOpen: false,
        modal: true,
        buttons: {
            "Close": function () {
                $(this).dialog("close");
            }
        }
    });

    // Sample product data
    $.get("/ProductsService.aspx", {},
        function (products) {
            _.each(products, function (product) {
                $('#products-table tbody').append(productTableTemplate({ product: product }));
            });
        }.bind(this));

    // Show the product dialog on row click
    $('#products-table tbody').on('click', 'tr', function (sender, event) {
        const id = new Number($(sender.currentTarget).find("input[type=hidden]").attr("value"));

        $.get("/ProductsService.aspx", {action: "detail", id: id},
            function (product) {
                $productDialog.html(dialogTemplate({ product: product }));
                $productDialog.dialog("open");
            }.bind(this));
    });
});

Fast and dirty

When we copy the content of the Products.aspx into a new DotVVM page, one issue is obvious. The DotVVM parses the template code islands as DotVVM bindings.

One of the possible strategies is to make as few changes as possible. We could use {{resource: }} binding to "escape" the templates. This solution will allow you to migrate the page very fast. In most cases we found out that the javascript on the page works correctly after this change, and the page was usable. There are certainly downsides with this solution. The solution is bit fragile, there is no advantage in maintainability and adding new features is just as bothersome as it was before. But if this is just some unimportant page, hardly used, that is holding you from switching to latest .NET, this solution might work for you.

Proper migration

If you want a full migration to DotVVM, it will be bit more involved process, but there are also good news. We will gain strong typing and we will be able to completely get rid of javascript for pages like. The first course of action is to refactor templates into DotVVM controls and replace JQuery UI dialog with quite easy DotVVM equivalent.

Plan of action is as follows:

  1. Create the Products.dothtml page without the templates
  2. Migrate the product-template into the DotVVM Repeater control
  3. Migrate the dialog into the ProductDialog markup control
  4. Migrate ProductsService.aspx to ProductsUIService for the on-demand loading of dialog content

Step 1: The page

Creating Products.dothtml page is quite straight-forward. We don't need any of the templates, we will deal with them separately. So we just need to copy over the table, and later we will add a custom control for the dialog.

@viewModel DotVVM.Samples.Migrated.Pages.Products.ProductsViewModel, TestSamples
    @masterPage Migrated/Pages/Site.dotmaster
    
    <dot:Content ContentPlaceHolderID="MainContent">
        <table id="products-table">
            <thead>
                <tr>
                    <th>Code</th>
                    <th>Name</th>
                    <th>Price</th>
                </tr>
            </thead>
            <tbody>
            </tbody>
        </table>
    </dot:Content>

In the the viewmodel, we prepare a collection to hold the products, and later we will add the property to accommodate the dialog.

public class ProductsViewModel : SiteViewModel
    {
        public List<Product> Products { get; set; }
    }

Step 2: The product table

It is time to take care of product-template template. We could move the content of this template to a custom markup control. But since the table is simple, we may be able to get away with using it directly as a content of <dot:Repeater>.

We look at the line 22 of the products.js JavaScript file;

function (products) {
    _.each(products, function (product) {
        $('#products-table tbody').append(productTableTemplate({ product: product }));
    });
}.bind(this);

We can see underscore.js being used to iterate products using each and fill containing element with new elements created by evaluating productTableTemplate underscore.js template on data provided by the data provided by the elements of the products array.

Such construction can be directly equated to DotVVM <dot:Repeater> control. The repeater iterates Products we provide as DataSource and uses provided ItemTemplate to fill the table. The data context for the ItemTemplate is the item of Products list.

We can see that the code islands of the product-template template look a lot like bindings, and this is precisely what we will do with them. We will turn them into value bindings. Since data context of the ItemTemplate of a <dot:Repeater> is the item of the Products lists itself we don't need to specify value: product.Something for the bindings we can just write value: Something. Also we do not need the <input type="hidden" ... /> because all the data are in the viewmodel.

After migrating our product-template to DotHTML and using the <dot:Repeater> instead of the _.each(...) our table in Products.dothtml looks like this:

<table id="products-table">
    <thead>
        <tr>
            <th>Code</th><th>Name</th><th>Price</th>
        </tr>
    </thead>
    <dot:Repeater DataSource={value: Products} WrapperTagName="tbody">
        <tr>
            <td>
                {{ value: Code }}
            </td>
            <td>{{ value: Name }}</td>
            <td>{{ value: Price }} EUR</td>
        </tr>
    </dot:Repeater>
</table>

Notice that we use the WrapperTagName="tbody" property to tell the repeater to use <tbody> tag as a container for the rows.

Step 3: The dialog template

The original Products.aspx page uses a dialog to display detailed information about each product. We find the content of the dialog in underscore.js template with id of product-dialog-template. The template is evaluated in JavaScript and the dialog is opened as we can see on line 34 and 35 of products.js file.

$productDialog.html(dialogTemplate({ product: product }));
    $productDialog.dialog("open");

Here, the raw HTML is being set into the div that will contain the dialog itself. JQuery library function dialog then builds the dialog and dialog overlay for us based on our content. Also note that the dialogTemplate template is initialized above in the javascript projects.js file:

const dialogTemplate = _.template($('#product-dialog-template').html());

We need to represent this functionality in in DotVVM. First we need to migrate the content of the product-dialog-template template into DotVVM markup. The template code islands are quite straight forward we just replace them with value bindings.

The migrated content of the product-dialog-template template:

<div class="product-dialog-template">
    <h3>{{value: Name }}</h3>
    <p>
        <span>Code:</span> {{value: Code }}
    </p>
    <p>
        <span>Price:</span> {{value: Price }} EUR
    </p>
    <p>
        <span>Description:</span> {{value: Description }}
    </p>
</div>
<dot:Button Click={staticCommand: IsVisible = false} Text="Close" />

For now, we just migrate the content we will deal with using it in the page later.

We added the "Close" button that we have seen in the dialog configuration on line 13 of the products.js file:

buttons: {
    "Close": function () {
        $(this).dialog("close");
    }
}

It's job is semantically the same. It just closes the dialog. The tricky part is that we need to look in the ASPX page for the template and look in the JavaScript for the dialog configuration.

Since the dialog has custom data to manage, best practice is to create ProductDialogViewModel.cs viewmodel for the dialog. The markup can guide us here. We start with empty viewmodel for the control and create the properties based on the bindings in the markup (CTRL+. and Create property in the viewmodel if you have pro version of the DotVVM for VisualStudio). Plus, for showing and hiding the dialog we use IsVisible property that we add to the dialog viewmodel.

ProductDialogViewModel.cs

public class ProductDialogViewModel
    {
        public string Name { get; set; }
        public string Code { get; set; }
        public decimal Price { get; set; }
        public string Description { get; set; }
    
        public bool IsVisible { get; set; }
    }

With the dialog viewmodel ready, we can put a property for the dialog data into our main viewmodel ProductsViewModel.cs.

public ProductDialogViewModel Dialog { get; set; } = new ProductDialogViewModel();

Next we need to add the functionality of a dialog. We have several options, and we will explore all of them.

  • DotVVM Bootstrap ModalDialog control
  • DotVVM Business Pack ModalDialog control
  • Vanilla DotVVM custom dialog

Step 3.1: Dialog in Bootstrap for DotVVM

If you have a license for Bootstrap for DotVVM, you can use the bs:ModalDialog control. We would simply put the migrated <div class="product-dialog-template">...<div/> inside of the dialog ContentTemplate. The buttons have a separate template so we put <dot:Button ... Text="Close" ... /> into the FooterTemplate. We would wire the DotVVM properties DataContext to the viewmodel property Dialog, and then we would use IsDisplayed={value: IsVisible} to control whether the dialog is shown or hidden.

The page Products.dothtml with bs:ModalDialog included should then look like this:

<dot:Content ContentPlaceHolderID="MainContent">
    <bs:ModalDialog DataContext={value: Dialog} IsDisplayed="{value: IsVisible}">
        <div class="product-dialog-template">
            <h3>{{value: Name }}</h3>
            <!-- ... The rest of the migrated content of the template from before -->
        </div>
        <dot:Button Click={staticCommand: IsVisible = false} Text="Close" />
    </bs:ModalDialog>
    
    <table id="products-table">
    ...
    </table>
</dot:Content>

The Bootstrap for DotVVM provides all the necessary functionality of the dialog making our job a bit easier.

Step 3.2: Dialog in DotVVM Business Pack

With DotVVM Business Pack, it is a very similar story. We would put the migrated content of <div class="product-dialog-template">...<div/> together with <dot:Button ... Text="Close" ... /> into the content of bp:ModalDialog. We would wire DotVVM properties DataContext to viewmodel property Dialog, and then we would use IsDisplayed={value: IsVisible}. This would save us from having a custom dialog.

We can put the bp:ModalDialog control into the Products.dothtml page and fill the HeaderTemplate, ContentTemplate, FooterTemplate:

<dot:Content ContentPlaceHolderID="MainContent">
    <bs:ModalDialog DataContext={value: Dialog} IsDisplayed="{value: IsVisible}">
        <HeaderTemplate>
            Product detail: {{value: Name}}
        </HeaderTemplate>
        <ContentTemplate>
           <div class="product-dialog-template">
                <h3>{{value: Name }}</h3>
                <!-- ... The rest of migrated content  -->
                <!-- from the template from before -->
            </div>
        </ContentTemplate>
        <FooterTemplate>
            <dot:Button Click={staticCommand: IsVisible = false} Text="Close" />
        </FooterTemplate>
    </bs:ModalDialog>
    
    <table id="products-table">
    ...
    </table>
</dot:Content>

The dialog migration is done, DotVVM Business Pack provides the necessary dialog functionality.

Step 3.3: Dialog in vanilla DotVVM

Making dialog in vanilla DotVVM is not complicated at all. It just requires a few tricks we can explore.

To keep our markup nice and tidy, we create new ProductDialog.dotcontrol control file with our already created ProductDialogViewModel.cs viewmodel. So we start the file with @viewmodel directive and copy the migrated content of the product-dialog-template template into the file. With this we should get a nice start for our ProductDialog markup control.

We must not forget to register the markup control in the DotvvmStartup:

config.Markup.AddMarkupControl(
    "cc", 
    "ProductDialog",
    "Migrated/Pages/Products/Controls/ProductDialog.dotcontrol");

After we have prepared the basic migrated content for ProductDialog, we need to add the functionality of a dialog.

All that said, creating dialog in vanilla DotVVM is not hard at all. We modify ProjectDialog.dotcontrol by wrapping the content in <div class="dialog"></div> to serve as dialog body. Then we add <div class="overlay" /> to serve as an modal dialog overlay.

Of course we need to define the styles for the dialog to function correctly on the page. For that we need to define our css classes overlay and dialog. You can do it in the same control file, but since the CSS ca be reused for all dialogs usually we but it in the separate css file.

.dialog-overlay {
    z-index: 100;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: #aaaaaa;
    opacity: .3;
}

High z-index will make sure our dialog and overlay is displayed well above content on the page position: fixed; top: 0; left: 0; width: 100%; height: 100%; will make our overlay cover the whole page corner to corner.

.dialog {
    z-index: 101;
    position: absolute;
    top: 35%;
    left: 35%;
    height: auto;
    width: 30%;
    border: 1px solid #999;
    border-radius: 4px;
    background-color: #fff;
    padding: 10px;
}

The z-index needs to be above the value in .dialog-overlay. position: absolute; top: 35% left: 35%; width: 30%; is here to display the dialog centered on the page.

The ProjectDialog.dotcontrol control should at this point be a viable dialog, now we need to add it to the Projects.dothtml page:

<dot:Content ContentPlaceHolderID="MainContent">
    <cc:ProductDialog DataContext={value: Dialog} />
    
    <table id="products-table">
    ...
    </table>
</dot:Content>

For the dialog data we add Dialog property in the ProductsViewModel:

public ProductDialogViewModel Dialog { get; set; } = new ProductDialogViewModel();

Step 4: Loading the data

Now, when we have the UI ready, we can migrate the data loading. We have to load the data for the page itself, and we need to load the data for the dialog.

Let's look at products.js. On line 21, we make a GET request to ProductsService.aspx with no parameters to load JSON with list of projects;

$.get("/ProductsService.aspx", {},
    function (products) { ... });

On line 32, we can see another request this time with parameters detail and id.

$.get("/ProductsService.aspx", {action: "detail", id: id},
    function (product) { ... });

We need to look at ProductsService.aspx and see the ASPX page code behind implementation. (ASPX markup just displays Json property as literal.)

public partial class ProductsService : Page
    {
        private readonly ProductFacade facade;
    
        public ProductsService()
        {
            facade = new ProductFacade();
        }
    
        public string Json { get; set; }
        protected void Page_Load(object sender, EventArgs e)
        {
            var context = HttpContext.Current;
            var action = context.GetQuery("action");
            var id = context.GetIntQuery("id");
    
            if (action == "detail" && id > 0)
            {
                Json = JsonConvert.SerializeObject(facade.Get(id) ?? new object());
            }
            else
            {
                Json = JsonConvert.SerializeObject(facade.List());
            }
        }
    }

This time we are lucky and both the detail operation and the list operations are nicely separated in the ProductFacade. In many projects where old ASPX pages are used as API to get JSON data, the actions could be one tangled mess. In that case, we would have to refactor the page and create our facade and refactor the code-behind close to something we can see here. That however would be whole another can of worms we are not going to open here.

Now we know how ProductsService.aspx works, and we can take ProductFacade and reference it in our ProductsViewModel. We can take the facade.List() that is used for the GET request with no parameters and use it to load the products in PreRender function of the ProductsViewModel viewmodel. There is no point in loading the project in JavaScript. We can load them in C# and have type safety and better error checking.

public ProductsViewModel()
    {
        facade = new ProductFacade();
    }
    
    public override Task PreRender()
    {
        if(!Context.IsPostBack) {
            Products = facade.List().ToList();
        }
        return base.PreRender();
    }

Notice that we only load the products on the first load of the page because DotVVM will take care of refilling the Products property on postbacks.

To load the dialog data on demand we can use DotVVM UI service in combination with static command.

The job of ProductsService.aspx will be taken by newly created ProductsUiService. The DotVVM UI Services can be referenced from the DotVVM pages by using @service directive. We can call a method of the UI service from static command binding and assign the result to Dialog property. This solution is ideal to load data od demand without needing to do a full postback.

public class ProjectsUiService
    {
        private readonly ProductFacade facade;
    
        public ProjectsUiService()
        {
            facade = new ProductFacade();
        }
    
        [AllowStaticCommand]
        public ProductDialogViewModel GetDialog(int id)
        {
            var project = facade.Get(id);
    
            return new ProductDialogViewModel
            {
                Code = project.Code,
                Description = project.Description,
                IsVisible = true,
                Name = project.Name,
                Price = project.Price
            };
        }
    }

The UI service is just simply uses the ProductFacade from ProductsService.aspx uses Get(...) to get the project and creates the dialog viewmodel. Notice that methods of the UI service intended to be called from static commands need to have [AllowStaticCommand] attribute.

To be able to reference the UI service in the DotHTML markup we need to register it in the DotvvmStartup.

public void ConfigureServices(IDotvvmServiceCollection services)
    {
        ...
        services.Services.AddTransient<ProjectsUiService>();
    }

Finally, we can reference the service in the Projects.dothtml:

@service productsService = DotVVM.Samples.Migrated.Pages.Products.ProductsUiService

On line 80 of products.js, we see a JQuery event registration:

$('#products-table tbody').on('click', 'tr', function (sender, event) { ... });

We can do the same job as JQuery .click() directly in our Products.dothtml markup file. We can use DotVVM static command binding. Having the Click binding directly in the markup is more intuitive for anyone reading the code. The JQuery .click(...) references a tr element inside of products-table tbody. Base on this we locate equivalent element in our Products.dothtml page, and add the binding like so:

<tr Events.Click={staticCommand: _root.Dialog = productsService.GetDialog(Id)}>

The static command binding will call GetDialog of the UI service on the server side (without doing full post back) and sets property Dialog in the root viewmodel on the client side.

Conclusion

By wiring in the click event of the table row the migration of our test page is complete. For our migrated DotVVM sample we crated new files:

Migrated
    Pages
        Controls
            ProductDialog.dotcontrol
            ProductDialogViewModel.cs
        Products.dothtml
        ProductsViewModel.cs
        ProductsUiService.cs

On top of that we reused ProductsFacade.cs. We don't need projects.js as we migrated all the JavaScript logic into C# and DotHTML. We also don't need ProjectsService.aspx. We replaced its functionality with ProductDialogViewModel and ProductsUiService.

This migration was a bit more complicated in a sense that it is not possible to just mechanically do text replaces. The principles and patterns used here however can be used generally.

In particular, the way to migrate underscore.js templates and then turn them into either DotVVM markup controls, or put them as a content of DotVVM template property of a control like <dot:Repeater>.

Another useful pattern is that we can replace on-demand data loading in JavaScript like JQuery .get(...) by using DotVVM UI Services in tandem with static command bindings to load the data on demand without full post back.

Milan Mikuš

I am Software Engineer at RIGANTI. I participate in the development of the DotVVM Framework, and I take care of the DotVVM for Visual Studio extension. I was also involved in several projects where DotVVM was used to modernize ASP.NET Web Forms applications.