App Modernization - Part #1: Migrate Web Forms code islands to DotVVM and .NET 6

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

This article is the first part of the series:

 

 

In recent weeks, RIGANTI has finished a project where we had to migrate a legacy ASP.NET Web Forms app to .NET 6. The application was written in C# and .NET Framework, it was using JQuery and Underscore libraries, and there was a big bunch of the business logic without detailed documentation. The reason for the migration was to be able to run the application on Linux and preserve most of the functionality. Full rewrite of the application would be too costly as the application was mostly an internal tool. The users preferred keeping the appearance and behavior of the application as close to the original one as possible.

In this series of articles, I’d like to share some insights and common situations you’ll run into when you use DotVVM for the modernization of legacy apps.

Migration of if, else if, for and foreach code islands

Let's imagine that we have an old e-commerce with a page for gathering customer feedback of a product:

<div id="ResultsDiv" runat="server">
    <table id="notes">
        <% if (!string.IsNullOrEmpty(Error)) { %><tr><td colspan="3" class="error"><%=Error %></td></tr><% } %>
        <% if(User.IsInRole("Manager")) { %>
        <tr>
            <td colspan="3">
                <% if(ProductId > 0) { %>
                    <a href="Products.aspx?productId=<%=ProductId%>">To product</a>
                <% } else if (CategoryId > 0) { %>
                    <a href="Categories.aspx?categoryId=<%=CategoryId%>">To category</a>
                <% } else { %>
                    <a href="Dashboard.aspx">To Dashboard</a>
                <% } %>
            </td>
        </tr>
        <% } %>
        <tr>
            <th>Date</th>
            <th>User</th>
            <th>Comment</th>
        </tr>
        <% if( Notes == null || Notes.Count == 0 ) { %>
        <tr>
            <td colspan="3">No notes entered
            </td>
        </tr>
        <% } else { %>
        <% foreach( var note in Notes ) { %>
        <tr>
            <td><%=note.CreateDate.ToString( "MM/dd/yyyy hh:mm" )%></td>
            <td><%=note.UserName%></td>
            <td><%=note.Text%></td>
        </tr>
        <% } %>
        <% } %>
    </table>
</div>

We have chosen DotVVM to migrate the e-shop because that way, we can use the old pages and the migrated pages side by side in the same web application, and after every page is migrated, we can switch the project to .NET 6.

Challenges

As we copy the content of the ASPX page into a new DotVVM page, we can see that the markup is just broken, and it would not be better the other way around. This is because the ASPX tooling cannot parse DotVVM bindings and likewise, DotVVM tooling does not understand ASPX code islands.

The ASPX code block are not recognized in DotVVM page

To convert the page markup in the DotVVM-friendly form we need to:

  • get rid of the code islands,
  • keep the hierarchy of the code blocks intact and represented is some kind of DotVVM markup structure.

On top of that, we want to be able to use batch replace and regular expressions to save as much manual work as possible. It would be a bad day if we had to manually recreate every section where content of the page is conditionally displayed or hidden.

Strategy

The if, else if, and else code blocks are used to display or hide content of the page, in case of for/foreach to display the same content multiple times.

One of our concerns is to make edits that keep the hierarchy of the code blocks. The second concern is that we can do a batch replace to save as much work as possible. The obstacle here are the closing brackets; <%}%>. They are not specific and can belong to any of the code blocks. Yet they are essential to keep the structure of the page intact. We need to choose a DotVVM control that would allow us to replace all the <%}%> with a closing tag. We also need some flexibility to replace different kinds of code blocks with opening tag.

DotVVM has the PlaceHolder control which does not render anything. If some of its properties are set, it will render only a Knockout JS virtual element in the resulting HTML. We can use the control's IncludeInPage property to show or hide the content based on some expression coming from the viewmodel.

We cannot tell whether the ASPX <%}%> is part of if, else or for block. Thus, we replace all code islands with PlaceHolder and then refactor for/foreach into Repeater controls later. The process goes like this:

- We replace all <%}%> to </dot:PlaceHolder>
- We replace <% if ( /*condition here */ ) { %> to <dot:PlaceHolder IncludeInPage={value: condition here}>. To capture the condition we can use regular expressions.
- We replace <% } else { %> to </dot:PlaceHolder><dot:PlaceHolder IncludeInPage={value: "TODO: negated condition(s) here"}>. Else block require finishing by hand.
- We replace <% } else if ( /*condition here */ ) { %> to </dot:PlaceHolder><dot:PlaceHolder IncludeInPage={value: condition here && "TODO: negated condition from previous if(s)"}>
- Lastly, we need to deal with for/foreach blocks. To keep the replacements compatible, we also replace those with something like <dot:PlaceHolder DataSource={value: whatever we managed to capture from the for/foreach block}>. This block is usually the most non-standard so we need to creatively craft the regular expression. On top of that, sometimes the index is used to generate row number in a table or row coloring. This needs to be commented out and dealt with by hand.

Few replaces later, we should have a DotVVM page looking like this:

<div id="ResultsDiv" runat="server">
    <table id="notes">
        <dot:PlaceHolder IncludeInPage={value: !string.IsNullOrEmpty(Error) }>
            <tr><td colspan="3" class="error"><%=Error %></td></tr>
        </dot:PlaceHolder>
        <dot:PlaceHolder IncludeInPage={value: User.IsInRole("Manager")}>
            <tr>
                <td colspan="3">
                    <dot:PlaceHolder IncludeInPage={value: ProductId > 0}>
                        <a href="Products.aspx?productId=<%=ProductId%>">To product</a>
                    </dot:PlaceHolder>
                    <dot:PlaceHolder IncludeInPage={value: CategoryId > 0 && "TODO: negated condition from previous if(s)"}>
                        <a href="Categories.aspx?categoryId=<%=CategoryId%>">To category</a>
                    </dot:PlaceHolder>
                    <dot:PlaceHolder IncludeInPage={value: "TODO: negated condition(s) here"}>
                        <a href="Dashboard.aspx">To Dashboard</a>
                    </dot:PlaceHolder>
                </td>
            </tr>
        </dot:PlaceHolder>
        <tr>
            <th>Date</th>
            <th>User</th>
            <th>Comment</th>
        </tr>
        <dot:PlaceHolder IncludeInPage={value:  Notes == null || Notes.Count == 0 }>
            <tr>
                <td colspan="3">
                    No notes entered
                </td>
            </tr>
        </dot:PlaceHolder>
        <dot:PlaceHolder IncludeInPage={value: "TODO: negated condition from previous if(s)"}>
            <dot:PlaceHolder DataSource={value: var note in Notes }>
                <tr>
                    <td><%=note.CreateDate.ToString( "MM/dd/yyyy hh:mm" )%></td>
                    <td><%=note.UserName%></td>
                    <td><%=note.Text%></td>
                </tr>
            </dot:PlaceHolder>
        </dot:PlaceHolder>
    </table>
</div>

The resulting markup is still far from being migrated, but now formatting and refactoring of the page has just became much more convenient. With that, let's correct the obvious issue caused by the replace:

  • Line 6 can be simplified. We have a special control for this task called dot:RoleView we replace PlaceHolder to RoleView and put Manager in the Roles property
  • On lines 9, 12 and 15, we have some TODOs to deal with. Unfortunately, we need to do this manually. This is just a logic exercise: we have to go top to bottom adding and negating the conditions as required.
  • On line 34, the former foreach can now became a dot:Repeater. We know that because we left the original content of the foreach in the DataSource property. DataSource does not exist on a PlaceHolder. But we can change PlaceHolder to Repeater and the tag hierarchy will be preserved. We can change the binding value: var note in Notes to just value: Notes because we can see it will just bind on a collection in the viewmodel. Finally, we need to add RenderWrapperTag="false" to the repeater(s) because dot:Repeater renders a div element by default. The div would break our table.
  • On line 33 the PlaceHolder  can be removed completely as it's condition is integrated in the Repeater control by default.
  • Next, we replace <%=SomeProperty %> with <dot:Literal Text={value: SomeProperty}>. There are few side notes to this. In the href="" attribute, we need to use interpolated string in the binding, because we cannot bind just half of the attribute. Inside the Repeater, we don't use value: note.Text in the bindings, but we simply use value: Text because the Repeater changed the data context for us. We need to craft a batch replace for such cases.
  • We added <tbody> in our table because browsers may try to add it in and break our virtual elements in the resulting page.
  • Finally, we can get rid of the runat="server". Make sure the elements marked with runat="server" are not being referenced directly form the code-behind. Dealing with code-behind is beyond the scope of this article.

After everything is done, we should now have a valid DotVVM markup. To make it work, I drafted out a viewmodel with properties. The resulting markup will not win any code beauty contests, but our goal here was to create a stable strategy for migrating 2000 lines of ASPX code quickly.

 <div id="ResultsDiv" runat="server">
    <table id="notes">
        <tbody>
            <dot:PlaceHolder IncludeInPage={value: !string.IsNullOrEmpty( Error ) }>
                <tr><td colspan="3" class="error">
                <dot:Literal Text={value: Error } /></td></tr>
            </dot:PlaceHolder>
            <dot:RoleView Roles="Manager">
                <tr>
                    <td colspan="3">
                        <dot:PlaceHolder IncludeInPage={value: ProductId > 0}>
                            <a href={value: $"Products.aspx?productId={ProductId}"}>To product</a>
                        </dot:PlaceHolder>
                        <dot:PlaceHolder IncludeInPage={value: CategoryId > 0 && ProductId == 0}>
                            <a href={value: $"Categories.aspx?categoryId={CategoryId}"}>To category</a>
                        </dot:PlaceHolder>
                        <dot:PlaceHolder IncludeInPage={value: CategoryId == 0 && ProductId == 0}>
                            <a href="Dashboard.aspx">To Dashboard</a>
                        </dot:PlaceHolder>
                    </td>
                </tr>
            </dot:RoleView>
            <tr>
                <th>Date</th>
                <th>User</th>
                <th>Comment</th>
            </tr>
            <dot:PlaceHolder IncludeInPage={value:  Notes == null || Notes.Count == 0 }>
                <tr>
                    <td colspan="3">
                        No notes entered
                    </td>
                </tr>
            </dot:PlaceHolder>
            <dot:Repeater DataSource={value: Notes } RenderWrapperTag="false">
                <tr>
                    <td>
                    <dot:Literal Text={value: CreateDate} FormatString="MM/dd/yyyy hh:mm" /></td>
                    <td>
                    <dot:Literal Text={value: UserName} /></td>
                    <td>
                    <dot:Literal Text={value: Text} /></td>
                </tr>
            </dot:Repeater>
        </tbody>
    </table>
</div>

After migrating few pages, you will get the process semi-automated. I wish you good luck migrating your legacy pages, and I will continue sharing our experience from Web Forms to DotVVM project migrations in the next parts of this series.

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.