One ASP.NET Sneak Peek: Elegant Web Forms and Snowballs in Hell
For the most part, I'm an ASP.NET developer. I don't need to specify MVC or Web Forms, because it's all One ASP.NET its core. My apps are often hybrids and include not just Web Forms or MVC but also SignalR and Web API.
Web Forms often gets picked on because of large View State, weird markup or maybe folks don't like the controls model. However, Web Forms has its place and it's getting even better with .NET 4.5. Here's a little sneak peek of some cool ideas Damian Edwards and the team have been working on for the next version of ASP.NET.
As a place to start, remember that ASP.NET routing started in MVC and moved into core ASP.NET. Routing is useful in all ASP.NET applications - MVC, Web Pages and Web Forms. Model Binding is coming to Web Forms as well, as well as Strongly Typed Data Controls and some other features that make both the code and the result pretty compelling. Dare I say, elegant. Elegant Web Forms? Madness! Who is this fool?
Here's a sortable grid with Create, Edit, Delete in Web Forms 4.5. An experiment for you, Dear Reader, would be to do the same thing I'm doing here in ASP.NET MVC or Web Pages.
Do note that this is fresh off Damian's laptop, and it's a experiment.
First, note the clean URLs. Use Routing, Web Forms people. You have no reason not to.
Here's what it'll look like:
Right now in this experiment, there is this routing table. Personally I'd like a convention for CRUD to make this one line.
Routes.MapPageRoute("CategoriesNew", "Categories/New", "~/Categories_New.aspx");
Routes.MapPageRoute("CategoriesEdit", "Categories/{id}", "~/Categories_Edit.aspx");
Routes.MapPageRoute("CategoriesDelete", "Categories/{id}/Delete", "~/Categories_Delete.aspx");
Routes.MapPageRoute("Categories", "Categories", "~/Categories.aspx");
Here's the Grid. Now, before you declare that's too freaky, take a look and note there's a lot of functionality going on here in not too many lines. ItemType (was ModelType in the Developer Preview) is strongly typing this grid to the Category model. Notice SelectMethod. You just need to provide a method here that returns an iQueryable, in this case, GetCategories.
UPDATE with NOTE: See the comments below. Damian Edwards says: "That said, you don't need to return IQueryable. You can happily return an IEnumerable and just take in the extra parameters that the GridView will give you to ensure you can retrieve only the data for the currently requested page and sorted by the chosen column."
<asp:GridView runat="server" ID="categoriesGrid" CellSpacing="-1" GridLines="None"
ItemType="VS11Experiment.Model.Category" DataKeyNames="CategoryId"
AutoGenerateColumns="false"
AllowPaging="true" AllowSorting="true" PageSize="5"
SelectMethod="GetCategories">
<Columns>
<asp:DynamicField DataField="Name" />
<asp:DynamicField DataField="Description" />
<asp:TemplateField>
<ItemTemplate>
<a runat="server" href='<%# GetRouteUrl("CategoriesEdit", new { id = Item.CategoryId }) %>'>edit</a>
<a runat="server" href='<%# GetRouteUrl("CategoriesDelete", new { id = Item.CategoryId }) %>'>delete</a>
</ItemTemplate>
</asp:TemplateField>
</Columns>
<EmptyDataTemplate>
No categories found.
</EmptyDataTemplate>
<SortedAscendingHeaderStyle CssClass="asc" />
<SortedDescendingHeaderStyle CssClass="desc" />
<PagerStyle CssClass="pager" />
</asp:GridView>
"OK, Hanselman, what hellish code-behind are you going to show us now? What Satan's spawn have you been shining us on with all this time only to spring the nasty stuff at the last minute? I know you crafty Microsoft types, always creeping around with your Legos and your Windows Phones."
Fine, you caught me.
public partial class Categories : System.Web.UI.Page
{
private readonly DemoWhateverDataContext _db = new DemoWhateverDataContext();
public void Page_Load()
{
if (!IsPostBack)
{
// Set default sort expression
categoriesGrid.Sort("Name", SortDirection.Ascending);
}
}
public IQueryable<Category> GetCategories()
{
return _db.Categories;
}
}
Aargh! My eyes! Wait, that doesn't suck at all. Even better if I could hypothetically put the default sort on the GridView and lose the whole Page_Load.
Whatever database or repository or Web (HTTP) Service you like, as long as your data access layer returns some IQueryables and you're in a good place. Sorting happens via LINQ so your data access layer can do the work, not ASP.NET.
So listing categories in a grid is decent, what's Edit look like?
If you add Model Binding to ASP.NET WebForms you spend less time digging around in the Request object. Notice that we're not doing that all here.
See how the RouteData attribute on GetCategory pulls the id out of the URL Categories/1?
public partial class Categories_Edit : System.Web.UI.Page
{
private readonly DemoWhateverDataContext _db = new DemoWhateverDataContext();
public Category GetCategory([RouteData]int? id)
{
return _db.Categories.Find(id);
}
public int UpdateCategory(int categoryId /* Comes from the data control itself via DataKeyNames property */)
{
var category = _db.Categories.Find(categoryId);
TryUpdateModel(category);
if (ModelState.IsValid)
{
return _db.SaveChanges();
}
return 0;
}
protected void categoriesForm_ItemUpdated(object sender, FormViewUpdatedEventArgs e)
{
Response.RedirectToRoute("Categories");
}
protected void categoriesForm_ItemCommand(object sender, FormViewCommandEventArgs e)
{
if (e.IsCancel())
{
Response.RedirectToRoute("Categories");
}
}
}
Often folks point to ASP.NET MVC's ability to use PRG (Post Redirect Get) as a strength. Often PostBacks in WebForms are looked down upon. In this model above, we're also totally able to use the PRG interaction model in Web Forms. See how the item is updated and we redirect to a route.
And the categoryId on UpdateCategory() comes from the Form View that is HTTP posting the data back. Here's a snippet:
<asp:FormView runat="server" ID="categoriesForm" RenderOuterTable="false"
ItemType="VS11Experiment.Model.Category" DataKeyNames="CategoryId"
DefaultMode="Edit"
SelectMethod="GetCategory" UpdateMethod="UpdateCategory"
OnItemUpdated="categoriesForm_ItemUpdated"
OnItemCommand="categoriesForm_ItemCommand">
Also, you know how in ASP.NET MVC you've got unobtrusive JavaScript validation that is driven by the model class itself?
In ASP.NET MVC one often uses EditorFor, and in Web Forms we've got Dynamic Control. The idea being that Dates get Calendars and you can replace the UI completely using a field template. That feature actually started in Web Forms Dynamic Data and then moved to ASP.NET MVC. Features move both ways when it's all ASP.NET underneath. See what I did there?
<EditItemTemplate>
<ol>
<li><label>Name:</label>
<asp:DynamicControl runat="server" ID="name" DataField="Name" Mode="Edit" />
</li>
<li><label>Description:</label>
<asp:DynamicControl runat="server" ID="description" DataField="Description" Mode="Edit" />
</li>
</ol>
...
And when these forms are POSTed, you'll need validation. Rather than Validation Controls, in this case since we already know about the model we can use unobtrusive validation, similar to ASP.NET MVC. The idea is push the best ideas into the core of ASP.NET and make common stuff easy while letting people work the way they want to work.
public class Category
{
[ScaffoldColumn(false), Display(Name="Id")]
public long CategoryId { get; set; }
[Required, StringLength(100)]
public string Name { get; set; }
[StringLength(10000), DataType(DataType.MultilineText)]
public string Description { get; set; }
}
Sure, ASP.NET Web Forms may not be your cup of tea just like ASP.NET MVC might not be either. But remember that it's all One ASP.NET, and you've got a number of tools in your toolkit. Pick the ones that make you happy.
*And no, Hell isn't a dirty word in this context. ;)
Related Links
- ASP.NET Web Forms vNext Videos
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
Presumably paging parameters can be passed back from the gridview? That Select method looks like it's returning a full dataset on every call.
On the second point: If you tie yourself down to IQueryable, no, you cannot just tear that piece out and use Dapper, Massive, or something else. You're tied to whatever LINQ provider you use. Although in theory you could replace providers, in practice doesn't always work this way. Not to mention that testing something like this would not be possible - not without a lot of work anyway.
I realize that tying down to IQueryable is somewhat limiting but NHibernate, l2s, and EFCF are decent choices. I'll see what a Massive or Dapper version would look like and report back. It would pass sorting in differently, of course.
Can you give me an idea of what you'd like to see around a best practice or big problem that I am not sharing on my blog? Or that I can work with the PMs to solve?
Also, is it still imposible to unit test "Categories_Edit" (code "behind")? I haven't used WebForms since ASP.NET 2.0, but if it's still a PIA, then I can only continue to suggest *NEVER* to use WebForms for anything "serious"
I currently am in the python/django/sqlalchemy/nginx/etc space, but come from more of a .Net background so can relate to your comment of "Ruby guys making fun of us." I don't think it has anything to do with code like the quick examples Scott puts together, but rather the few that like to complain and not show the proper examples when they do it. I have actually been fairly successful in getting quite a few of my companies LAMP developers to embrace .Net platforms, but it took doing it by example.
With all due respect, saying "This is no less separated than if you put data-access code in the body of your MVC action method" is not really that true. I can put this kind of code in an MVC controller action method, call it in a unit test and ensure that my view will be handed the correct data before it renders (yes, I know that would be running the tests straight against the db).
Last time I Googled/Binged "ASP.NET Webforms Unit Testing" I didn't get many useful results. Whereas "MVC3 Controller Unit Testing" has a lot more there!
I actually like what you've demo'd here. But I think there's a lot of us out there that want to see where the testing hooks will be in Webforms.
@Marcel: Exactly
@Chris: I do not blog, not much of a writer, but I'll leave my two cents every now and then; that helps a bit, I like to think... I've "converted" a few myself, so I know where you're coming from.
Now imagine the state of web technology over a decade ago when Web Forms led an army of VB6 developers to the world wide web. Now jump back to the present. See the issue? The problems that Web Forms was designed to solve (which it very well in its day) NO LONGER EXIST.
Web Forms had its day. Let's move on. Microsoft already has a very viable v3 MVC web framework that should really be their prescriptive web development platform.
James
@Scott - How strongly typed are these controls? Is the ControlBuilder actually building a FormView<Category>? If so, what properties are generic on these new controls? Would setting the SelectMethod in code-behind be type-safe? Does the CreateItem() method signature include dataItem as Category or still as Object?
Also, have you guys considered a server-side MVVM approach for WebForms where ViewState is completely turned off and only the ViewModel is persisted (using ControlState)? This would allow ViewModels to be reused between WebForms and WPF apps.
Are you referring to the private CreateItem method on things like Repeater and ListView? Because I can't find a public/protected method of that name that takes an object parameter called dataitem.
As for your last suggestion, that's basically what Web Forms MVP (http://webformsmvp.com) gives you today. I've built very large complex websites using Web Forms MVP where viewstate was disabled and all rendering was done from the view model in a single data-bind call just before the render stage. As the author of Web Forms MVP and the Program Manager for Web Forms at Microsoft, that's still my recommendation to people who want to get the highest separation of concerns in a Web Forms application with all the benefits of a pluggable IoC system and testability of presentation logic thanks to System.Web.Abstractions, while still keeping the benefits of the rich component and control system for behavior encapsulation and UI composition.
For example I could use validation groups. A way to say that these fields "are together". When you have big forms showing the validation summary for all the fields in one place can be confusing.
Following this path, on the server side, having a way to unbind and validate in separate steps and by groups would also be nice. For example, sometime I hide part of the form via JS and on the server side I don't want to unbind and validate those fields. Unbinding would be OK, but definitely not the validation part. You could specify the properties to include/exclude with TryUpdateModel, but it's clunky imo.
Another nice-to-have would be to have message types, at least: error, warning, and information. Being able to programmatically add a "warning" for example to one field or one group of fields. For example: Your account expires in X days if you choose this option.
There are workarounds, you can make it work, but I haven't yet found a clean / elegant solution. I'm not looking for something that automates everything for me. I'm looking for the right balance between being sufficiently flexible and writing the minimum amount of code. In a way, I'm looking for the jquery-of-the-validation-problem.
RenderOuterTable="false"
Sigh... the ASP.NET WebForms developers just got smart too late, the framework will always be haunted by its past.
Personally, I would like to see how these new features affect performance
"As the author of Web Forms MVP and the Program Manager for Web Forms at Microsoft, that's still my recommendation to people who want to get the highest separation of concerns in a Web Forms application ...."
If you are talking about http://webformsmvp.com, which is part of the Outercurve foundation, then how about getting some documentation out on that? That website has been like that for more than ayear, and the wiki is like an abandoned ghost town. It may be very good, but it sure doesn't make a solid impression.
You're right, I was confusing the way CreateItem/CreateRow works on the Repeater/GridView with how I would like it to work, or even better I suppose would be if RepeaterItem.DataItem and GridViewRow.DataItem were typed and always available (due to the bound ViewModel data being persisted and rebound on each postback).
What would be the ultimate would be if the designer generated partial inner classes on the page/user control for templates so that the controls in item templates (for rows on the Repeater or cells on the GridView) could have designer generated fields just like controls outside of templates, with this class having overridable page life cycle methods to manipulate those controls in a strongly-typed way.
Sorry, but there's nothing they can do to WebForms to make me go back.
i'am a vb.net programmer
please wrote your code in vb.net
thank you
sorry for my bad english.
Here is my question/concern. Although I also use MVC and other frameworks, I have been building webforms sites for close to 10 years. I can't even estimate how many times I have used the ObjectDataSource. My concern lies with the iQueryable thing. Will there be a more direct migration path for users that simply want to rip out an ObjectDataSource and move directly to the model binding approach without having to rewrite the data layer to use iQueryable? I guess this kind of lines up with some of the micro ORM questions as well. Will we be able to call a method such as "GetCategories(sortBy)" the same way we did using an ObjectDataSource?
Thanks,
John
Look people it is up to you to write code that is clean and well seperated. Regardless of what framework you are using! WebForms has it's warts, but you can write maintainable code using it, and you can also write horrible code in MVC and Rails. If you really want to, implementing an MVP pattern that works in WebForms takes five minutes... literally!
In the end your choice of framework does not automatically make your code better or worse. It might make it easier/harder for you to write more maintainable code, but it doesn't preclude you from using your noggin. Writing clean maintainable code is an exercise in diligently thinking about your particular architecture and making informed decisions on how to keep concerns separated. No framework or technology can do this for you!
As far as Scott's example. It's meant to demonstrate the technology... not extraneous things like testing, encapsulating domain logic, validation, separation of concerns, etc...
At the end of the day MS job is to improve the experience with the technology. What developers do with it is up to them.
I feel a blog post coming on...
You seem to be attributing to emotion what is NOT just an emotional response to WebForms. Not all ways of doing things are equal. I contend that WebForms is simply a flawed way of abstracting web development into a framework, when compared with MVC. I'm willing to talk about the specifics, but please do not mistake that we are poor workmen blaming our tools.
Of course it's possible to write bad code under any framework. I simply claim that under WebForms it's easier to do things that will be difficult to correct later, *because* the abstractions and components do not allow the proper degrees of freedom to correct issue or because interactions between them have more cases of hard to fix issues.
Richard
I'm mostly an MVC or Web Pages guy thesesl days, myself. But the truth is that MVC usage is *maybe* 10% of ASP.NET usage right now. There are smart people doing interesting work on Web Forms today who are interesting in the fact that Web Forms CAN about clean HTML, do HTML5, use jQuery, do model binding, be testable with WebFormsMVP, and most importantly IS useful. You don't have to convert - I'm not asking you to. Just be aware it's out there and while you may not like its abstraction, others do.
I know you love MVC, but the fact is that ASP.NET (as I say "One ASP.NET") is what I'm interested in making better. You can be an enthusiast about one architecture or another, but it's all ASP.NET underneath. I move freely between them and choose to self-identify as an ASP.NET developer.
In fact, perhaps it's my limited capacity that makes me yearn for a less abstract web framework, where some truly smart folks can make WebForms work for them because the added complexity (IMO) is not too much for them.
In the end, frameworks are for US, the developers, so we should choose what we like.
I thought to myself "If I was the owner of a company, and a client wanted simple crud, would I consider this?". And yes, I probably would. It looks like it might be the most profitable route for my non-existent software company.
Good stuff
So does paging generate fresh urls or is it handled via postbacks? If I navigated to page 2 and then pressed F5, would I have to confirm that I wanted to re-post?
Why not have this UpdateCategory method?
public int UpdateCategory(Category category)
{
if(ModelState.IsValid) {
return _db.Save(category);
};
return 0;
}
public IList<Category> GetCategories(string sortByExpression, int maximumRows, int startRowIndex, out int totalRowCount)
{
totalRowCount = _db.Categories.Count();
var categories = _db.Categories
.SortBy(sortByExpression)
.Skip(startRowIndex)
.Take(maximumRows)
.ToList();
return categories;
}
@David Wong - the GridView continues to use post backs to perform things like sorting and paging. In this sample, we've just put the GridView in an UpdatePanel by itself, so that doing sorting or paging won't force you to do a full post. That, coupled with the separate forms for edit, create, etc. result in nice PRG experience for this simple example.
@Scott Koon - yes that pattern is supported too. I tend to discourage it for updates against models with collection properties, which is typical if you're using EF code first model classes directly, as the model binder will be unable to populate the collection obviously and as such your object will have no children, causing the relationships to get deleted if you save it. But for the Create scenario here, we could've done that no problem.
Making an existing DAL return IQueryables is a harder problem. You really need to use an ORM that has a LINQ query provider so that the IQueryable returned will actually transform the composed query into raw SQL that will executed on the database, not in your app tier. Things like Entity Framework, Linq to SQL and NHibernate have support for LINQ and thus IQueryable.
That said, you don't need to return IQueryable. You can happily return an IEnumerable and just take in the extra parameters that the GridView will give you to ensure you can retrieve only the data for the currently requested page and sorted by the chosen column.
What's wrong with you guys. Has anybody taken the time to do what Scott said at the beginning and do this in ASP.NET MVC or Web Pages? That's the power of WebForms. The DataControls. Always has been. You don't like the IQueryable, write your own class and properties to separate anything you want. That's not the point in this article. I too prefer MVC, because i like clean html and http but i used to be a WebForms developer and i also like the way the whole .NET is evolving. Stop acting like you have to maintain facebook's database and keep up with the simple things that make a difference.
DEAR LORD....
1. This is SO testable.
2. This will SO prevent N+1 errors
3. This SO lends itself to inversion of control.
"This" is not the thing described in this post.
Really doesn't seem to be dead.
Obviously not all of us have the luxury of moving to MVC due to company policies etc. so we still have to use WF in production environments, but I know and love WF as it is now, I've learnt NOT to shove everything into the ASPX code and decided to write code-behind that is understandable to me and other users of it.
Push MVC if you think it's the next evolutionary step, but don't drag a 3-legged dog along with it, make your choice and stick with your roadmap as I think you are beating a dead-horse with WF personally. You guys did this with VB when .Net came along, it was always left behind C# but you still insisted on pushing it (I can see why, safe strategy and all), you have continued the VB/C# divide and you are now doing it with WF/MVC, it's nice to have a choice and all but sometimes I think you guys should just grab the bull by the horns, it sends mixed signals to the community IMO.
I think this is key information and it is badly missing from the post which only says "You just need to provide a method here that returns an iQueryable, in this case, GetCategories.".
But the whole thing is very different if there are really two options: return IQuaryable or take in extra parameters and return IEnumerable.
You can use the first one to implement quick and simple apps and the second one to have more control and create something more complex (with proper separation of concerns and all).
Attila
I would really love to read about displaying SSRS reports in an MVC application. I always end up adding a WebForms page with a ReportViewer control.
Oh, and if you happen to post about validation best practices (as Peter suggested), I think it would be great to include some complex examples, like:
Inter-property dependency: The valid value of a property depends on the value of another property on the same object.
Inter-object dependency: The valid value of a property depends on the value of another property on a different (but related, e.g. parent-child) object.
Many developers here don't realize that those upgrades are really really important for us, legacy site maintainers... They can make our life much easier!
Now I don't know if this has changed with more recent versions of ASP.NET, someone please enlighten me.
A little of topic, but I noticed you mentioned in the text of the post about the Web API: What is this project, and could you provide a link?
Also, in the image at the end of the post, there is 'Single Page Apps' rectangle: Does it refer to one actual page, or single-page Ajax applications? If Ajax, is this an MS project with a web site?
Thanks,
Put a download link ( or maybe my eyes are not good) and I do the same in MVC ;-)
Thank you,
Andrei
Thanks
Great post and appreciate all the work you guys are putting into asp.net. What I think is really missing in webforms is TempData (this is already in MVC). I don't think we can fully realize the PRG Pattern without it. For example you say
"Often folks point to ASP.NET MVC's ability to use PRG (Post Redirect Get) as a strength. Often PostBacks in WebForms are looked down upon. In this model above, we're also totally able to use the PRG interaction model in Web Forms. See how the item is updated and we redirect to a route."
However, what happens when we want to add a simple success message when the update has happened ? Because there is no Tempdata (flash scope .. whatever) the RPG pattern in webforms starts to break down real quick. I think the changes you are doing are fantastic additions to webforms but please consider making Tempdata a first class citizen too! It would really help realize the RPG dream in webforms.
End of life webforms development(support yes enhance no).
http://blog.brickred.com
We welcome your thoughts, opinions, Comments on our blog.
http://cc.bingj.com/cache.aspx?d=4568857797530172&w=915a7ef2,d6c256f4
I also really like the default sample that comes with it, I was able to get a personal page up and running in about 2 afternoons and that includes some special features I put in like displaying recent blog posts from another domain (with output caching!) or being able to generate my list of projects in different formats (e.g. text format for some carreer websites). Without any prior knowledge of ASP.NET MVC!
Big thanks to the ASP.NET MVC team. In many years this was really the first time I actually enjoyed developing a web site (feel free to forward them this compliment).
P.S.: Razor rocks!
Example:
<%@ Page Title="Home Page" Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication2._Default" %>
<asp:TextBox Text='<%: Page.MyCustomProperty %>' runat="server"></asp:TextBox>
namespace WebApplication2{
public partial class _Default : Page
{
public string MyCustomProperty {get; set;}
}
}
What is going on here. I am using VS2012.
Problem is, I cannot find a way to use a DropDownList in the InsertItemTemplate; I can use it in the EditItemTemplate using SelectedValue="<%#: BindItem.ClientStatusID %>", but that syntax is not allowed in the InsertItemTemplate.
Net result is that when I TryUpdateModel in the EditMethod, the value of the DropDownList is honoured, but if I do the same in the InsertMethod the value is ignored (i.e. not set on the model).
Comments are closed.