Archives
-
Migrating a large web application to ASP.NET MVC
Recently I had the luck of migrating some classic ASP pages to ASP.NET MVC. I would share some of the experiences below.
ASP.NET webform and ASP.NET MVC can coexist just fine
We have a huge existing ASP.NET webform application with thousands of pages. The challenge therefore is that any addition developed using ASP.NET should not affect the operation of existing webform pages. The mixing of ASP.NET Webform and MVC has been studied previously by many people. It is fairly easy to find some articles by searching “asp.net mvc and webforms together” on your favorite search engine. Basically, we need to include 3 additional DLLs in the web application:
- System.Web.Routing
- System.Web.Abstractions
- System.Web.Mvc
The System.Web.Routing is not part of ASP.NET MVC, but is used by MVC to route a web request to a controller. The System.Web.Abstractions add a wrapper to some built-in asp.net classes such as Request, Response, Session to decouple MVC code from the ASP.NET built-in classes so that these classes can be mocked in testing projects.
The first step to enable routing is to add the URLRoutingModule to the list of HTTP Modules in web.config:
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule,System.Web.Routing, Version=3.5.0.0,Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
The next step is to configure the route. In order not to interfere with the operation of existing asp.net pages, we want to exclude all of them from routing in global.asax:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
routes.IgnoreRoute("{resource}.asmx/{*pathInfo}");
routes.IgnoreRoute("{resource}.svc/{*pathInfo}");
routes.IgnoreRoute("{resource}.ashx/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
In the last line, we used an ASP.NET feature called Areas that I will discuss later.
The third step is to add a bunch of namespaces to <Pages> in web.config so that some MVC namespaces can be used in MVC view pages. We skipped this step because we do not want to affect the existing pages with bunch of new namespaces. We will do this as part of the Area implementation.
ASP.NET MVC allows 100% of the code in the library DLLs
In order to allow developers work independently, we actually split the application into multiple web applications. By default, ASP.NET MVC project put models, views and controllers in a single project. It is in fact very easy to move models and controllers into a separate library DLL, leaving the web application project only the view pages. On deployment, we simply deploy the DLLs to the bin directory of the application root, and the view pages into directories under the Areas folder (see below).
The ASP.NET MVC Areas feature is one of the best feature in MVC 2 for large projects
There are many resources about MVC Areas on the internet. For example, one can read Phil Haack’s original article on the subject. With areas, the responsibility for registering route is delegated to a subclass of AreaRegistration which now lives in our library project. When AreaRegistration.RegisterAllAreas() is called, ASP.NET will search for all AreaRegistration subclasses in all assemblies in the bin dir so that we do not have to modify the global.asax when we add new areas.
In the AreaRegistration subclass, we would do something like:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Services",
"Sub1/Sub11/Sub111/{controller}.mvc/{action}/{id}",
new { action = "Index", id = "" },
new string[] { "My.Custom.Namespace.Controllers" }
);
}
Note that we use an optional parameter to route the request to the controllers under particular namespace so that we can retain our existing namespace structure. Also, this approach allows us to blend the new mvc URLs into the existing application URLs very well.
Our web applications are only left with views. We just need to deploy these views to the web server under appropriate area directories. This is how an Areas directory may look like:
Note that we have a web.config in the Areas directory? That is the place we add a HttpNotFoundHandler to protect all the views from getting accessed directly. We also add the appropriate attributes, and namespaces to the <Pages> element so that we apply these configurations only to the view pages.
Summary
In summary, ASP.NET MVC allows us to put 100% of code into many library dlls that can be independently developed. The areas feature allows us to delegate the responsibility of route registration to each individual dll as well as centrally apply namespaces and attributes to view pages without affecting the existing asp.net webform pages.
-
PublicSettingsSingleFileGenerator
I added a Settings.settings file in one of my library project and I want to use the settings in my ASP.NET application. Unfortunately, the Settings class in Settings.Designer.cs is created as internal sealed partial class so that my ASP.NET application in a different assembly cannot see the class. Fortunately, to make the Settings class public, all I need to to is to go to the properties of Settings.settings file and change the Custom Tool from SettingsSingleFileGenerator to PublicSettingsSingleFileGenerator.
-
System.ComponentModel.DataAnnotation talk at LA Code Camp
Today, I gave a talk at LA code camp on using Validation Attributes in the System.ComponentModel.DataAnnoation namespace in the context of both ASP.NET MVC and other applications.
Since .net 3.5 does not have a Validator class, I have supplied a Validator class in the attached code. In addition, I have also presented a data structure that can be constructed dynamically to stored the validation attributes for classes and properties, and a dynamic validator that consume the data structure.
-
Awarded Microsoft ASP/ASP.NET MVP
Since I became an MCSD charter member in 1995, I finally reached a new height in my career. I had a physics major, but I was very fascinated by computer when I got my PC. I only took a C++ course in college; at that time C++ “compiler” was still a C translator. The class project was to write a C++ pretty printer; that is how I learnt parsing and data structure. I immediately applied the knowledge to my first consulting assignment. My client wants to rewrite a DOS program to a Windows program, but the company that wrote the DOS program would not give them the source code. Fortunately, all the data in the program is available through several reports so my assignment was to parse the printer language so that I can extract the data. In 1995 to 1996, I wrote a VBA like interpreter and a mini web browser, all in 16 bit VB3. In 1995, I was also invited to Redmond to write the Microsoft Access 95 exam. Then I worked comfortably for a insurance software company for 9 years.
In 2006, I had a chance to work with a very innovative team at ESRI. There I was able to apply my parser skill again on a very innovative project called Site Builder. It is essentially a wizard that can generate a map website like Windows Live map. What makes it interesting is that it can reverse engineering an existing website by parsing ASP.NET pages to extract information and allow user to modify the website through the wizard.
In 2007, I returned to the insurance industry. I noticed that due to various cross-cutting requirements, developers often turn a one page rating spec into 3000-5000 lines of code. There I started experiment with domain specific language (DSL). I was able to express the rating logic in only 20-30 lines of code, with almost one-to-one correspondence to the written spec. Then I can execute the DSL with a runtime that has only about 600 lines of code. Any new cross-cutting requirement can be implemented in the templates. As the company was moving from Microsoft technology to Java, I was sent to the Java team. There I worked with Struts, Spring and Hibernate. Although I quickly gained 3 Java certifications in a few months, I felt I missed .NET. I would say simply piling up best frameworks and best practices would often lead to bloated rather than best software.
In 2008, I joined an insurance software vendor that is a .Net shop. The shop still has a large number of classic asp pages. So I started writing ASP Classic Compiler using Microsoft dynamic language runtime (DLR). My previous experience with Struts allows me to exam ASP.NET MVC very thoroughly. I am very glad that ASP.NET MVC, through several quick iteration, has exceeded Struts. I am very glad that ASP.NET MVC has made effort to reduce repetitive code. For example, you can just add a few DataAnnotation attributes to the entity, the validation rule can even propagate to the client Javascript code! I also like to see the new generation of user-friendly tools like LightSwitch. I was slightly disappointed that LightSwitch was tightly coupled to Silverlight. I was hoping there would be an abstraction on top of the actually WPF classes. This way the tool could not only generate the desktop software, but also true Web AJAX application; I think both Google Web Toolkit (GWT) and Script# has demonstrated its feasibility. We have been changing UI frameworks too often. If the tool put reasonable abstraction over the UI and tier, developers just need to fill business logic in a few places in the template. The build system can then generate whether we want desktop, web or a new type application that we are not even aware of today. If we have been working with a more abstracted toolkit, we could probably work with the tool for much longer than jumping through different frameworks.
-
My first real-world experience with Web Client Software Factory (WCSF)
Introduction
WCSF has existed since January 2007. Although I looked at it before, this is the first time I used it in a real project. Since ASP.NET MVC has been available, a frequent question for a development team is the choice between WCSF or MVC. I would not comment on how our team arrived the decision but will comment on the comparison at the end of this post. In this post, I will attempt to document what I learnt, both for myself and for anyone who encounter the similar situation.
What is WCSF
Web Client Software Factory (WCSF) is an application framework that works with Microsoft ASP.NET webform. In a typical webform application, there is a significant amount of logic in the code behind file that takes input from request, server controls and call business components. The the code would set the state of the server components or redirect to another URL based on the return of business components. The code is also responsible for reading and setting the states such as session, application and viewstate. The most important objective of WCSF is to separate the logic from the code behind file into the module project as much as possible so that these logic can be tested using Visual Studio test project or another testing framework such as NUnit.
WCSF projects can be created using a WCSF Visual Studio extension. The new extension contains a new WCSF solution template:
Once the solution is created, projects can be added to the solution using the add module wizard:
Module is a basic organization unit in WCSF. A module actually contains several projects. The following picture shows the organization of the Contacts module:
As seen in the picture above, when we add the Contacts module, WCSF adds a folder Contacts in the web project and also add a Contacts library project. When we add a web page, WCSF adds a view interface and a presenter class into the Views folder of the module project. WCSF uses model-view-presenter (MVP) pattern so that the responsibility for each page is divided between a model, a view and a presenter. I have seen people talking about the complexity and the steep learning curve of WCSF. However, some knowledge of the lift cycle would make it much easier to understand.
WCSF Lift Cycle
Whether we create a new WCSF web site or modify an existing web site to work with WCSF, the first thing is that the the Web Application must inherit from Microsoft.Practices.CompositeWeb.WebClientApplication or its subclass. This can be done by setting the following in the global.asax page:
<%@ Application Language="C#" Inherits="Microsoft.Practices.CompositeWeb.WebClientApplication" %>
This will allow WCSF to participate in the application initialization process. Specifically, it will search the Web.Config in the application root directory and sub directories to find WCSF configuration. A typical WCSF configuration section looks like:<compositeWeb>
<modules>
<module name="Contacts" assemblyName="MVPWithCWABQuickStart.Contacts" virtualPath="~/Contacts">
</module>
</modules>
</compositeWeb>The configuration inforamtion tells WCSF to load the module assembly and run the code in the ModuleInitializer class.
A page or a web control that participate in the WCSF life cycle needs to inherit from the Microsoft.Practices.CompositeWeb.Web.UI.Page or the Microsoft.Practices.CompositeWeb.Web.UI.UserControl class. This allows the object builder to instantiate the presenter/controller objects and inject them into the view through dependency injection.
The WCSF web page and the Model-View-Presenter pattern
Whenever we add a page to the web project, WCSF will also create a view interface and a presenter. The web page inherits from the Microsoft.Practices.CompositeWeb.Web.UI.Page class and implements the view, as see in the following code snippets:
public partial class ContactsList : Microsoft.Practices.CompositeWeb.Web.UI.Page, IContactsListView
{
private ContactsListPresenter _presenter;
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
this._presenter.OnViewInitialized();
}
this._presenter.OnViewLoaded();
}
[CreateNew]
public ContactsListPresenter Presenter
{
set
{
this._presenter = value;
if (value != null)
{
this._presenter.View = this;
}
}
get
{
return this._presenter;
}
}
}The inheritance from the Microsoft.Practices.CompositeWeb.Web.UI.Page class would allow the page to participate in the life cycle of a WCSF page which uses ObjectBuilder to inject dependent class when the page is constructed. The CreateNew atribute instructs ObjectBuilder to construct a new instance of ContactsListPresenter and assign it to the ContactList class. WCSF creates the view and presenter does not enforce how the model is created. There are two flavors of Model-View-Presenter (MVP) pattern used in WCSF: PassView and Supervising Controller.
With Passive View, the Presenter is responsible for interacting with the business layer (the Model in this case). The view interface needs to expose every fields that the presenter would read and set. The web page would simply expose the control values as get and set properties. For example, the interface:
public interface IContactsListView
{
int SelectedIndex { get; set;}
}and the implementation:
public int SelectedIndex
{
get { return CustomersGridView.SelectedIndex; }
set { CustomersGridView.SelectedIndex = value; }
}In the Supervising Controller, the Model is often a View Model that contains the data to be presented. The presenter would pass an instance of view model to the view and view is responsible for rendering the view model, often through a data binding mechanism. For example:
public interface IContactDetailView
{
void ShowCustomer(Customer customer);
}Comparing the two flavors, the Passive View requires a large view interface with many members. To add a new field, you would need to modify both the view interface and the view implementation. In contrast, with supervising controller, you would only need to modify the view model to add a new field. However, some data binding logic are handled in the view and is not testable.
On Modulization
In our project, we have a fairly complicated page that is furthered spitted into several user controls developed by several developers at the same time. Each user control is a view by itself and has its own presenter. However, these user controls often need to share the same time and communicate with each other. That is where the Controller comes into the picture. Fortunately, the WCSF project already contains a controller and the presenter template already contains the code to inject the controller; we only need to uncomment the code. For example:public class ContactsListPresenter : Presenter<IContactsListView>
{
IContactsController _controller;
public ContactsListPresenter([CreateShared] IContactsController controller)
{
_controller = controller;
}
public IContactsController Controller
{
get { return _controller; }
}
…
}In order to have all the user controls share the same instance of the controller, we have to change the dependency-injection attribute from CreateNew to CreateShared. This way, the entire request would share the same copy of the controller. The CreateShared attribute as well as the code that needs to be added to global.aspx can be downloaded from the WCSF contrib project.
With all the user controls getting the same instance of the controller, the user controls can interact with each other by calling controller methods and subscribe to events fired by the controller.
Unit Testing
One major objective of WCSF is that the classes can be unit tested outside of a web server. This would requires creating mock view that implements the view interfaces to interact with presenters and controller. For this reason, one want to leave out as much as logic from the code behind as possible so they can be unit tested. In addition, the module project should be free of reference to request, session and other web server objects so that they can be tested outside a web server. The WCSF has StateValue<T> and StateDependency classes for injecting session value into the module classes. The WCSF contrib also has a QueryStringValue<T> class for injecting the query string parameters.
Impression
My impression is that WCSF works with an existing web form application very well. It can be used to split large project or complicated pages into smaller pieces to be worked simultaneously. We did not encounter any significant limitation with WCSF.
The next question is whether we want to WCSF or ASP.NET MVC since this is a FAQ. The choice is really between WebForm and ASP.NET MVC. WCSF is just a way to make WebForm project more organized. I would say that WebForm is still the most productive framework (although it does not produce the most optimal HTML code). WebForm is a mature framework and has a abundant third party controls. If you have limited budget, performance is not major concern, or you use lots rich dhtml controls like treeview, WebForm is still the way to go. On the other hand, if you have a bigger budget, needs to product optimal Html code, or is concerned with the future growth of the project, and are comfortable to deal with the occasional situation of not able to find some server controls so that you have to use some JQuery client-side controls, then MVC is the way to go.
-
How to enable tracing in ASP Classic Compiler
With the build 0.6.2, we added the option to enable tracing. The tracing can be enabled by setting the following in the global.asax.cs file:
using Dlrsoft.Asp;
AspHandlerConfiguration.Trace = true;Setting this option will tell the compiler to inject line number into the compiled code. If the compiled code throws any runtime error, the exception will contain the source file name and line number.
If you would like to receive the complete trace history, you will have to configure a trace listener. VBScript.net compiler uses TraceSource “Dlrsoft.VBScript”. The following is an example configuration section in the web.config file:
<system.diagnostics>
<trace autoflush="true">
<listeners>
</listeners>
</trace>
<sources>
<source name="Dlrsoft.VBScript"
switchName="vbscriptSwitch"
switchType="System.Diagnostics.SourceSwitch">
<listeners>
</listeners>
</source>
</sources>
<switches>
<add name="vbscriptSwitch" value="Verbose"/>
</switches>
</system.diagnostics>The above configuration will send the trace information to DefaultListener. You can see the trace output using DebugView. You might also route the trace output to ASP.NET tracing using the WebPageTraceListener. Please see this post for more information regarding tracing.
Please note that enable tracing itself only adds a small overhead to the Asp Classic Compiler. However, some listeners can add a huge overhead.
-
Using custom .net classes in ASP Classic Compiler
To use custom .net classes, I extended VBScript syntax with the Imports statement. The Imports statement introduce the top namespace to the VBScript, as in the example below. The following example demonstrates using ADO.NET in ASP Classic Compiler:
<%
imports system
dim filePath = Server.MapPath("/Database/authors.mdb")
dim oleConn = new system.data.oledb.oledbconnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & filePath)
oleConn.Open()dim cmd = new system.data.oledb.OleDbCommand("SELECT * From authors", oleConn)
dim dr = cmd.ExecuteReader()
do while dr.Read()
response.Write(dr.GetString(1) & "<br/>")
loop
%>In the example above, “imports system” instructs VBScript.net that system is a namespace rather than an object. When VBScript.net encounters system.data.oledb.oledbconnection, it follows the namespace hierarchy to find the oledbconnection class.
However, VBScript.net does not automatically search all the loaded assemblies. Instead, it only searches the assemblies it was instructed search. VBScript.net will always search the system.dll assembly. However, to instruct VBScript.net to also search in the system.data.dll, we need to add the following code to global.asax.cs:
using System.Reflection;
using Dlrsoft.Asp;…
protected void Application_Start(object sender, EventArgs e)
{
Assembly a1 = Assembly.Load("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
AspHandlerConfiguration.Assemblies.Add(a1);
}The above code tells AspHandler to search the System.Data.dll assembly for classes.
Lastly, to saves the typing of the fully qualified class name, the “imports” statement also supports alias. The following code demonstrates using alias to save typing:
<%
imports t = system.textdim sb = new t.StringBuilder()
sb.append("this")
sb.append(" is ")
sb.append(" stringbuilder!")
response.write sb.toString()%>
-
Uploaded ASP Classic Compiler Build 0.6.1.34834
It has been a long time without a new release. This release contains only a few small fixes, but it is a change in the strategy. I decided to branch the code: one branch to stabilize the VBScript 4.0 features and the other branch to implement the VBScript 5.0 features such as Eval/Execute and VBScript classes.
The reason is that not all users need VBScript 5.0 features. Some users have only a small number of VBScript classes and can easily convert them to C# classes as VBScript.net can use .net classes. Although I still have a lot to do to reach a mature release, some users only use limited VBScript features so they can use ASP Classic Compiler first. I will issue a license under the “Early Access Program” for those users who want to go live with pre-release versions of ASP Classic Compiler.
The “Early Access Program” is an enterprise/ISV class of program with source code access. It allows participating users to self-support under emergency situations with source code access. Although the compiler code may have a steep learning curve, many users can learn simple bug fixes fairly quickly by following bug fixes in the source control system. The remaining users can still get the binaries from the Codeplext site http://aspclassiccompiler.codeplex.com without warranty. If you are interested in the program, please contact me using the contact user form.
-
When will the next version of ASP Classic Compiler be released?
I am not released a new version for a while. I received a few inquires regarding the next release. The answer is a few weeks to a few months.
The first reason is that I have a fulltime job working for a software development company as an ASP.NET developer. We are in a very busy phase in the past couple of months. On top of that, my kids have several major math competitions coming up and I had to spend sometime to help them. So I have less time and that breaks my momentum.
The second reason is that I am actually planning a moderate rewrite. My original plan was to implement the features up to VBScript 4.0 in the first version. However, I got feedback from users that they have to have Eval/Execute feature in VBScript 5.0. That would requires me to lift stack variables to heap so that they can be consumed by Eval/Execute. I found that I have to implement the variable lifting myself to handle the language semantics accurately, just like Iron Python and Iron Ruby teams have done. So I am in fact taking over the memory management from DLR. I will have a much more flexible architecture, be able to do the runtime debugging and be able to optimize the code with many techniques. As this is the second iteration, I want to do things much better. I have been doing lots of theoretical studies and benchmark works lately. I think I will be able to put together a much better product when I get my next break.
-
To talk about C# 4.0 dynamic feature and the Dynamic Language Runtime at SoCal Code Camp
I am very excited that I will be giving a talk at SoCal Code Camp tomorrow, Jan. 31, 2010. I am going to talk about C# 4.0 dynamic feature and Dynamic Language Runtime, the same subject that I talked about 3 months ago at LA Code Camp. But this time, I am going to put it under the context of building extensible application and present road map and options. I will also talk about the ASP Classic Compiler (http://aspclassiccompiler.codeplex.com) implemented using Dynamic Language Runtime. The presentation material can be downloaded here.
I added a couple new samples:
- Anonymous: Show how to leak anonymous type using dynamic type.
- Overload: Show the difference between compile time and run time method resolution.