Plug-In Hybrids: ASP.NET WebForms and ASP.MVC and ASP.NET Dynamic Data Side By Side
I wanted to write this post because there seems to be a lot of confusion about how ASP.NET WebForms, MVC and Dynamic Data all fit together. Folks may have WebForms applications and want to start looking at ASP.NET MVC but aren't sure if they need to start from scratch or not. Folks might look at Dynamic Data but mistakenly assume it's just for scaffolding administrative sites.
When you're in Visual Studio and you hit "File | New Project" you're given a number of choices. This is a starting point, but it easy to assume that this point is a fork in the road and you can mix them up.
You can (and should) feel free to have Hybrid applications. You can have single Web applications that have all of these inside them (if it makes you happy):
- ASP.NET MVC
- ASP.NET WebForms
- ASP.NET Web Services (ASMX)
- WCF Services
- ASP.NET Dynamic Data
- ASP.NET AJAX
Here's an extreme example. I'll pick ASP.NET MVC Application to start with, but it doesn't really matter. If you're confused as to what web.config entries are required, just make one of each kind of project and compare the files with your favorite diff tool (mine is Beyond Compare).
Ok, so first I've got a Hello World ASP.NET MVC application:
I'll add a quick LINQ to SQL database connection to the AdventureWorksLT database so I have something to query.
Next, I'll throw a quick ASMX Web Service into this ASP.NET MVC application that returns some data. I create it via File | New Item and select Web Service. I make a quick LINQ expression and a smaller class SmallProduct (a LINQ projection) that is returned. I also make it ScriptService, so I could call it via AJAX if I liked.
namespace Overkill
{
public class SmallProduct
{
public string Name { set; get; }
public string Color { set; get; }
public decimal Price { set; get; }
}
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[System.Web.Script.Services.ScriptService]
public class Products : System.Web.Services.WebService
{
[WebMethod]
public List<SmallProduct> GetProductsByColor(string color)
{
AdventureWorksDataContext d = new AdventureWorksDataContext();
return (from p in d.Products
where p.Color == color
select new SmallProduct
{
Name = p.Name, Color = p.Color, Price = p.ListPrice
}).ToList<SmallProduct>();
}
}
}
What does this have to do with ASP.NET MVC? Nothing. That's the point. This is an ASP.NET 2.0 style ASMX Web Service with an ASP.NET AJAX ScriptService attribute using a .NET 3.x LINQ Query to return the data, all living in an ASP.NET MVC application.
Why doesn't ASP.NET MVC grab the request? Two reasons. First, there's an option on RouteCollection called RouteExistingFiles. It's set to false by default which causes ASP.NET MVC to automatically skip routing when a file exists on disk.
if (!this.RouteExistingFiles)
{
string appRelativeCurrentExecutionFilePath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (((appRelativeCurrentExecutionFilePath != "~/") && (this._vpp != null)) && (this._vpp.FileExists(appRelativeCurrentExecutionFilePath) || this._vpp.DirectoryExists(appRelativeCurrentExecutionFilePath)))
{
return null;
}
}
Because the default Route in Global.asax isn't greedy enough to care even if we were routing all requests:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
However, in my personal experience, File.Exists is a very expensive operation (very is relativeā¦it's expensive because it hits the disk at all). A best (and example of attention to detail) practice would be to put in an IgnoreRoute call for those pages, directories, and/or HttpHanders. For example:
routes.IgnoreRoute("{myWebForms}.aspx/{*pathInfo}");
routes.IgnoreRoute("{myWebServices}.asmx/{*pathInfo}");
routes.IgnoreRoute("myCustomHttpHandler.foo/{*pathInfo}");
routes.IgnoreRoute("Contents/{*pathInfo}");
Here I'm ignoring .aspx, .asmx, a custom HttpHandler with a custom extension and the whole of the Contents folder. I might even want to set routes.RouteExistingFiles = true which would turn off the File.Exists check and put ALL the pressure for routing on my routes. I'll need to be more careful and explicit, but that's rarely a bad thing. You could also just structure your site such that all your non-MVC things live in their own folder. It's up to you.
Now, let me add a WebForm and *gasp* drag a GridView into it. Wow, I'm a bad person, I've just used the Designer. I was productive so, but what price my immortal soul? ;)
Seriously, though, use what makes you happy. This grid is kind of lame, so I'll add some ASP.NET DynamicData. However, while you usually see Dynamic Data from a File | New Application point of view, I'm going to just bring a DynamicDataManager control onto the page. You'll also want to confirm that you have DynamicData controls listed in your web.config:
<pages>
<controls>
<add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add tagPrefix="asp" namespace="System.Web.UI.WebControls" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add tagPrefix="asp" namespace="System.Web.DynamicData" assembly="System.Web.DynamicData, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</controls>
</pages>
I'll add a few things to my WebForms page:
<div>
<asp:DynamicDataManager ID="DynamicDataManager1" runat="server" />
<asp:GridView ID="GridView1" runat="server" DataSourceID="GridDataSource"
AutoGenerateColumns="false" DataKeyNames="ProductID" AllowPaging="true" AllowSorting="true">
<Columns>
<asp:DynamicField DataField="Name" />
<asp:DynamicField DataField="ProductNumber" />
<asp:DynamicField DataField="Color" />
<asp:DynamicField DataField="ListPrice" />
</Columns>
</asp:GridView>
<asp:LinqDataSource ID="GridDataSource" runat="server" EnableDelete="true"
ContextTypeName="Overkill.AdventureWorksDataContext" TableName="Products"/>
</div>
In the Global.asax.cs, I'll add these two lines to let the DynamicData system know that we're working on this DataContext:
MetaModel model = new MetaModel();
model.RegisterContext(typeof(AdventureWorksDataContext), new ContextConfiguration() { ScaffoldAllTables = false });
Then, the most important part, I'll want to bring in the ~\DynamicData folder, since that's where DynamicData finds all of its templates. For this example, I really only need ~\DynamicData\FieldTemplates as I'm only using the smallest bit of Dynamic Data functionality.
To do this easily and quickly, I usually make a throwaway new DynamicData Web Application in another instance of Visual Studio, making sure to give it the same name (and hence, namespace) as the one I'm working on. Then I just drag that project's DynamicData folder over into my original application, and ensure that all the designer files and code-behinds are included in the project (Show All files, then right click each one, Include in Project). The rumor is that there will be a quick way in the future to bring a fresh DynamicData folder into an existing app.
Now, I'll hit my page again and then I get shiny Dynamic Data goodness.
At this point I've got a WebForm, Dynamic Data, and a totally random unused WebService living inside an ASP.NET MVC application. Of course, now this begs the question "Is this an ASP.NET MVC application."
Oh, you wanted MVC used also? ;) I'll add a quick Products method to the HomeController:
public ActionResult Products(string color)
{
AdventureWorksDataContext d = new AdventureWorksDataContext();
var smallProducts = (from p in d.Products
where p.Color == color
select new SmallProduct
{
Name = p.Name,
Color = p.Color,
Price = p.ListPrice
}).ToList<SmallProduct>();
return View("Products", smallProducts);
}
Then a quick view, making sure it's derived from ViewPage<List<SmallProduct>>:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Products.aspx.cs" Inherits="Overkill.Views.Home.Products" %>
<%@ Import Namespace="Overkill" %>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<p>
<ul>
<% foreach (SmallProduct p in ViewData.Model)
{ %>
<li><%=p.Name %> <%=p.Price.ToString("C")%></li>
<% } %>
</ul>
</p>
</asp:Content>
And that works also, visiting /Home/Products, ensuring there's a route that matches. I'll make this Route overly specific:
routes.MapRoute(
"ProductsWithColor",
"Home/Products/{color}",
new { controller = "Home", action = "Products", color = "Red" }
);
And it renders like this:
I hope this helps and it's more clear now that it's just "an ASP.NET application."
You can feel free to mix and match. Not everyone can (or should) rewrite an existing ASP.NET application, so it is nice that everyone can use some new features in the same ASP.NET application as their existing functionality.
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
david
Also, if different asp.net technologies have to share some files (temporary files, for example) I find it much cleaner to have them all under one single Web Application.
Yes, you can "bolt them together" yourself (I did it with Dynamic Data Futures + existing Dynamic Data), but it's not easy (nor fun), so hurry up and make MVC and Dynamic Data Futures full participating members of .Net 3.x.
Now, let me add a WebForm and *gasp* drag a GridView into it. Wow, I'm a bad person, I've just used the Designer. I was productive so, but what price my immortal soul? ;)
But Scott, you work for Microsoft, haven't you already sold your soul?? ;)
But Scott, you work for Microsoft, haven't you already sold your soul?? ;)
Yes, but when you the Designer the Devil gets a bonus :p
Can see an example with PHP, RUBY, ASP.NET WebForms, ASP.NET MVC, Perl, MonoRail all mixed into one application, I am interested doing somethin like this but not know where to start.
Please Scott? Help
Thanks for a great post. This definately shows the power of the new tools found in MS VS2008 SP1 :) I just showed 2 or 3 dev's how DD is an awesome tool out of the box and this takes it even further...
win.ini
--------------------------------
; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
yada yada
ASP.NET is headless development and we want things that work not new new technologies every day.
Have you seen anything on allowing MVC/Dynamic Date site coexisting with URLRewriting.NET.
Thanks,
Michael
If you don't want to learn any new technology, you shouldn't be working in this field...
the T is not necessary as the projection (IEnumerable<T>) already knows the T ;)
.ToList();
just works
putting the stuff together this way also shows that the project templates thing is kind of broken... as more features come out, it will inevitably become bloated and continue to give the idea that those features are silos...
How about a reworked template that is more intelligent and extensible by current and future functionality? Something like "New ASP.NET Application" which brings a dialog that "products" can extend to inject maybe a checkbox to add features, or maybe an entire wizard page if there are some settings needed for a feature, etc...
Putting together the pieces by creating throwaway apps is no fun... Although it's true you do it only once per project...
In some previous forums you mentioned that there will be some information how could third party orm developers create custom Data context for this? (actually I'm interested of only Nhibernate version :)
How would you go about the other way around? Starting with a non-MVC ASP.NET web application and what needs to be done to have it run MVC components. I have been experimenting and can't seem to find a good way. I think that this scenario is more frequent than what you describe here, considering how many "classic ASP.NET" applications are out there. Can you advise?
Thanks,
Laurent
Thanks!
So we can asure that they can only be used when enabled (I know the standard ASP.NET will always be available :-) ), but also that all references, folders and minimal files are configured in the current project.
When this is done, the MVC will work but you will not be able to select MVC related items when you try to add them to your project. Therefor close your solution and open the project file of your web application and replace the existing <ProjectTypeGuids> tag with the following: <ProjectTypeGuids>{603c0e0b-db56-11dc-be95-000d561079b0};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
{603c0e0b-db56-11dc-be95-000d561079b0} = MVC Items
{349c5851-65df-11da-9384-00065b846f21} = can anyone tell me??
{fae04ec0-301f-11d3-bf4b-00c04f79efbc} = Default ASP.NET
Mark, its not about learning a new technology.. MVC is not a new technology! Its about which technology was the best to go with, for me I've moved to ASP.net because I trusted Microsoft and liked the standards it follows, but today it seems that standards are getting modified! lets hope Microsoft will do it right this time..
MVC is not a replacement for web forms. Just think of it as a new tool in the web developer's tool box. Web forms aren't going anywhere.
But if you already have a large WebForms app using some different url re-writing scheme it will be difficult to add MVC because you would need a busload of routes.IgnoreRoute
So for example in mojoPortal CMS content system pages are served by a WebForm page Defalt.aspx?pageid=x, but we have a lot of friendly urls mapped to that one physical .aspx page with different pageids. So it seems we would need to add an routes.IgnoreRoute for every one of the friendly urls and the more pages we have in the site the more unwieldy that would become.
So maybe is not too bad in projects where most of your pages are real physical webform pages, but trying to introduce MVC as an option in a large existing app with a pre-existing url re-writing scheme seems problematic.
If you have any additional tips for this kind of scenario, would love to hear them.
Best,
Joe Audette
Comments are closed.
Also, the fact that those template and new file type options don't alphabetize by default drives me crazy.