Scott Hanselman

ASP.NET Web Forms DynamicData FieldTemplates for DbGeography Spatial Types (plus Model Binders and Friendly URLs)

September 11, '12 Comments [16] Posted in ASP.NET | ASP.NET MVC | Javascript | Open Source | VS2012
Sponsored By

Did you enjoy my recent post on ASP.NET MVC DisplayTemplate and EditorTemplates for Entity Framework DbGeography Spatial Types and it's associated GIANT URL?

Modeling Binding and EditorTemplates...for ASP.NET Web Forms?

DisplayTemplates and EditorTemplates are a great way in ASP.NET MVC to keep things DRY (Don't Repeat Yourself.) That means I can just write EditorFor() calls like this:

@Html.EditorFor(model => model.Location)   

See how I didn't say "TextBoxFor" or "MapFor"? You say EditorFor and it makes the right choice. If the type is called DbGeography then it will look for a Editor Template at ~/Shared/EditorTemplates/DbGeography.cshtml. It's a nice feature of ASP.NET MVC that folks don't use enough.

Now, remember ASP.NET Dynamic Data? You might think that idea "died" or was "retired" when actually the concepts are built into ASP.NET itself. That means that ASP.NET Web Forms developers can have "Editor Templates" as well. They are called FieldTemplates in ASP.NET Web Forms parlance, and making sure we have feature parity like this is part of the larger move towards One ASP.NET. We'll take the ASP.NET MVC sample using DbGeography and make it work for Web Forms in a very similar way.

<%--  Let's not do this: <asp:TextBox ID="location" runat="server" Text="<%# BindItem.Location %>"></asp:TextBox>--%>
<asp:DynamicControl runat="server" ID="Location" DataField="Location" Mode="Insert" />

When we do a POST, ModelBinders handle the boring work of digging types out of the HTTP POST. These work in not just MVC but also Web Forms and Web API now. Rather that Request["this"] and Request["that"] a model binder can be registered to do the work of populating a type from the Request. Even better, we can populate objects not only from the POST but also anywhere that provides values including Cookies, QueryStrings and more.

Let's walk through this one by one and at the end we'll have a complete sample that has:

  • ASP.NET Web Forms and AS.NET MVC in one application, living together.
  • FriendlyURLs for Web Forms and Routing for MVC
  • 90% Shared Model Binding Code between Web Forms and MVC
    • Spatial types custom DbGeography Model Binder
  • Simple CRUD (Create, Read, Update, Delete) to the same database in both Web Forms and MVC using the same model.

The goal is to continue to move towards a cleaner, more unified platform...One ASP.NET. This is an example. Thanks to Pranav for his help!

Related Links

DbGeography FieldTemplates for ASP.NET Web Forms

Here's a FormView in ASP.NET Web Forms. Notice the ItemType is set, rather than using Eval(). We're also using SelectMethod rather than an ObjectDataSource.

<asp:FormView runat="server" ItemType="TouristAttraction" ID="attractionDetails"
SelectMethod="attractionDetails_GetItem">
<ItemTemplate>
Name:
<asp:DynamicControl DataField="name" runat="server" ClientIDMode="Static" /><br />
Location:
<asp:DynamicControl DataField="location" runat="server" /><br />
<a id="A1" href='<%# FriendlyUrl.Href("~/WebForms/Edit", Item.TouristAttractionId ) %>'>Edit</a> |
<a id="A1" href='<%# FriendlyUrl.Href("~/WebForms") %>'>Back To List</a>
</ItemTemplate>
</asp:FormView>

The FormView doesn't specify what a location or name should look like, but since we know the model...

public class TouristAttraction
{
public int TouristAttractionId { get; set; }
public string Name { get; set; }
public DbGeography Location { get; set; }
}

...it will dynamically figure out the controls (hence, DynamicControl) and find FieldTemplates in the DynamicData folder:

Dynamic Data Field Templates called DbGeography.ascx

Those templates are simple. Here's the Edit example.

<%@ Control Language="C#" CodeBehind="DbGeography_Edit.ascx.cs" Inherits="MvcApplication2.DynamicData.FieldTemplates.DbGeography_EditField" %>

<asp:TextBox runat="server" ID="location" CssClass="editor-for-dbgeography" />

You might say, hang on, this is just a text box! I thought we weren't using TextBoxes? The point is that we have control in a single place over what a DbGeography - or any type - looks like when it's being edited, or when it's read-only. In this example, I AM using a Textbox BUT I've added a CssClass that I will use to create a Google Map using obtrusive JavaScript thanks to my recent refactoring from Dave Ward. If I wanted I could change this FieldTemplate to be a 3rd party control or whatever custom markup I want.

If you have an object called Foo, then make a Foo.ascx and Foo_Edit.ascx and put them in ~/DynamicData/FieldTemplates and they'll be used by DynamicControl.

Model Binding for ASP.NET Web Forms

I did 13 short videos recently on new features in ASP.NET 4.5 including one on Model Binding for ASP.NET Web Forms. Here's the Model Binding one.

Let me first say that Model Binding between ASP.NET Web Forms, MVC and Web API isn't unified. I want more unification and I am continuing to push the One ASP.NET vision internally and many people share that goal.

In the previous blog post on ASP.NET MVC, Model Binding and DbGeography I already had a good Model Binder that I want to reuse between MVC and Web Forms. I can do it, although the ModelBinderProvider stuff isn't very well unified.

First, here's the unified Model Binder for DbGeography that is used for both MVC and Web Forms. We implement two interfaces and use one implementation. Not ideal, but it works.

public class DbGeographyModelBinder : IMvcModelBinder, IWebFormsModelBinder
{
public object BindModel(ControllerContext controllerContext, MvcModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
return BindModelImpl(valueProviderResult != null ? valueProviderResult.AttemptedValue : null);
}

public bool BindModel(ModelBindingExecutionContext modelBindingExecutionContext, WebFormsModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.Model = BindModelImpl(valueProviderResult != null ? valueProviderResult.AttemptedValue : null);
return bindingContext.Model != null;
}

private DbGeography BindModelImpl(string value)
{
if (value == null)
{
return (DbGeography)null;
}
string[] latLongStr = value.Split(',');
// TODO: More error handling here, what if there is more than 2 pieces or less than 2?
// Are we supposed to populate ModelState with errors here if we can't conver the value to a point?
string point = string.Format("POINT ({0} {1})", latLongStr[1], latLongStr[0]);
//4326 format puts LONGITUDE first then LATITUDE
DbGeography result = DbGeography.FromText(point, 4326);
return result;
}
}

Part of the "trick" are these namespace aliases:

using IMvcModelBinder = System.Web.Mvc.IModelBinder;
using IWebFormsModelBinder = System.Web.ModelBinding.IModelBinder;

using MvcModelBindingContext = System.Web.Mvc.ModelBindingContext;
using WebFormsModelBindingContext = System.Web.ModelBinding.ModelBindingContext;

I'd love to see unification of the Model Binding stack at some point.

In Web Forms we could register a single model binder for a single type like this:

ModelBinderProviders.Providers.RegisterBinderForType(typeof(DbGeography), new DbGeographyModelBinder());

Or collect a collection of like types into a Provider of Binders and add them like this:

ModelBinderProviders.Providers.Insert(0,new EFModelBinderProviderWebForms());

I only have one Model Binder but here's how I'd register a provider for both Web Forms and MVC and have them use my same binder if I wanted:

public class EFModelBinderProviderMvc : System.Web.Mvc.IModelBinderProvider
{
public IMvcModelBinder GetBinder(Type modelType)
{
if (modelType == typeof(DbGeography))
return new DbGeographyModelBinder();
return null;
}
}

public class EFModelBinderProviderWebForms : System.Web.ModelBinding.ModelBinderProvider
{
public override IWebFormsModelBinder GetBinder(ModelBindingExecutionContext modelBindingExecutionContext, WebFormsModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(DbGeography))
return new DbGeographyModelBinder();
return null;
}
}

Now, to finish the CRUD.

FriendlyUrls

The team released an alpha build of ASP.NET FriendlyUrls that includes cleaner URLs, easier Routing, and Mobile Views for ASP.NET Web Forms yesterday. I wanted to use them in this project as well, and have WebForms and MVC together in the same app.

I could certainly register a bunch of Web Forms routes manually like this:

RouteTable.Routes.MapPageRoute("Attraction", "WF/Attraction", "~/WebForms/Default.aspx");
RouteTable.Routes.MapPageRoute("AttractionNew", "WF/Attraction/Create", "~/WebForms/Create.aspx");
RouteTable.Routes.MapPageRoute("AttractionEdit", "WF/Attraction/Edit/{id}", "~/WebForms/Edit.aspx");
...and more...

Or I could enable FriendlyUrls after my MVC routes like this:

//MVC will be for MVC, while WebForms is under /WebForms/ using Friendly URLs
routes.MapRoute(
name: "Default",
url: "MVC/{controller}/{action}/{id}",
defaults: new { controller = "Attraction", action = "Index", id = UrlParameter.Optional }
);

routes.EnableFriendlyUrls();

Here's what my site looks like now. Notice the /MVC and /WebForms URLs. I can call /WebForms/Create or /MVC/Create..

MVC and Web Forms together in one app

I generate the FriendlyUrls like this in Web Forms:

<a href='<%# FriendlyUrl.Href("~/WebForms/Edit", Item.TouristAttractionId ) %>'>Edit</a>
| <a href='<%# FriendlyUrl.Href("~/WebForms/Delete", Item.TouristAttractionId ) %>'>Delete</a>
| <a href='<%# FriendlyUrl.Href("~/WebForms/Details", Item.TouristAttractionId ) %>'>Details</a>

and like this in MVC

@Html.RouteLink("Edit", "Default",new {Controller="Attraction", action="Edit",id=item.TouristAttractionId}) |
@Html.RouteLink("Details", "Default",new {Controller="Attraction", action="Details",id=item.TouristAttractionId})|
@Html.RouteLink("Delete", "Default",new {Controller="Attraction", action="Delete",id=item.TouristAttractionId})

If this was a larger app I would write better helper methods for both, perhaps using an open source helper library.

UPDATE: One thing I forgot to mention was how to get the values out of the FriendlyURL. You can use things like [Form] and [QueryString] to model bind in WebForms. Now you can add [FriendlyUrlSegments] to get data out, like the ID in this example:

public TouristAttraction attractionsForm_GetItem([FriendlyUrlSegments]int? id)
{
TouristAttraction touristattraction = db.TouristAttractions.Find(id);
return touristattraction;
}

Both sections talk to the same database and use the same shared Google Maps JavaScript.

MVC and Web Forms together in one app

I chose not to try to share the _Layout.cshtml and Site.Master, although I could share Razor views and Web Forms.

I've updated the my playground repository with a single project that contains all this. Hope it helps.

https://github.com/shanselman/ASP.NET-MVC-and-DbGeography

Related Links

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. I am a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Tuesday, September 11, 2012 2:52:43 AM UTC
Hi Scott,

How would you use EditorTemplates in ASP.NET Web Pages 2 (CSHTML)? Would you import the MVC namespaces that make it work?

BTW, your "I could share Razor views and Web Forms" link at the end of the article is broken.

Cheers,
Nuri
Tuesday, September 11, 2012 6:10:05 AM UTC
Hi Scott, one problem I found when I implemented the pretty URLs with MapPageRoute() for my ASP.NET Web Forms app, was that some pages expected arguments in the query string. I could not find a way to pass URL Parameters into the querystring of the ASPX page.

Therefore I had to rewrite the codebehind of some pages to parse the RouteData.Values collection instead of the Request.QueryString property. It would be great if ASP.NET FriendlyUrls could implement this "querystring forwarding" feature, to avoid the need to rewrite pages.

And I have to say I really like the automatic redirect of the FriendlyUrls package. The nice thing of the routes is that it works in addition to the already existing pages (be it that you don't parse query strings). So you can roll-out the new routes side-by-side. With the redirect you can enforce the new routes when you're ready to deprecate the old URLs.

This way, the users of our reporting system did not need to be informed of the new URLs. They simply started to notice better URLs when copy/pasted from the address bar. And after a while when they are used to it, I can enable the redirect. Perfect!
Tuesday, September 11, 2012 1:26:16 PM UTC
Nuri,
Scott created a nuget package which I think should be the source of the broken link:

http://nuget.org/packages/AddMvc3ToWebForms

Not sure if you were aware or not, but just mentioning it. Scott, any chance this package will be updated (or a new one created) for MVC4? I'm going to need to do this in the future.
Tuesday, September 11, 2012 6:20:49 PM UTC
Too bad you can't mix the DynamicControl and the strongly typed modelbinding in the new Databound Controls. It would be nice to be able to write someting like this:

<asp:DynamicControl DataField="<%#BindItem.Name %>" runat="server" ClientIDMode="Static" />
Tuesday, September 11, 2012 7:23:44 PM UTC
Lodewijk - I don’t understand what you're trying to do here. DataField isn’t meant to be bound to, you set it to a static value. You can absolutely use DynamicControl with strong typed/model binding.
Tuesday, September 11, 2012 7:42:34 PM UTC
Perhaps I didn't make myself clear. I love the new <%# Item.PropertyName %> databinding in 4.5 because it gives me the compiler and Intellisense, which is much nicer than Eval("PropertyName").

But the Dynamic Data controls remain in 'Magic String'-land with DataField="PropertyName".
Wednesday, September 12, 2012 3:38:43 PM UTC
Lodewijk - I see what you're saying now. I'm thinking we could look at providing IntelliSense for DataField using the currently set ItemType on the parent data control (we actually give IntelliSense for the ItemType property too this way). Making it support an actual expression instead of a string (like the MVC HTML helper methods) is quite a lot harder but is something we can investigate.
Wednesday, September 12, 2012 7:15:33 PM UTC
//4326 format puts LONGITUDE first then LATITUDE

Just a clarification: EPSG 4326 doesn't define coordinate ordering. The (Longitude,Latitude) ordering in Well-Known Text (WKT) is there because WKT was defined for (X,Y) coordinates on a plane, not 3-dimensional coordinates on the surface of an ellipsoid. Since it is an ambiguous definition in the spec, most implementations are using the same concept as (X,Y) on the Cartesian Coordinate System, which is happens to be X=Longitude, Y=Latitude.
Wednesday, September 12, 2012 9:21:50 PM UTC
Jason - AH! Thanks for the explanation. Sorry for my ignorance.
Wednesday, September 12, 2012 9:31:15 PM UTC
Damian - Thanks, it's just a little detail, but it would give modelbinding the same api for all controls, dynamic or regular.

Now if webforms was open source, I could send a pull request ;)
Monday, September 17, 2012 6:43:38 PM UTC
Scott - do I have to upgrade to 4.5 to use Dynamic Data with Code First? I've just tried in 4.0, and it's ignoring my model configuration and giving me a foreign key cycles error, even though I've configured the affected foreign keys to not cascade.
Richard
Monday, September 17, 2012 6:47:26 PM UTC
Richard - Yes, I believe so but best to ask on the aspnetwebstack.codeplex.com discussions site.
Monday, September 17, 2012 6:52:21 PM UTC
OK, thanks. I was hoping I'd done something wrong!
Richard
Tuesday, September 18, 2012 12:00:45 PM UTC
Turns out I was doing something wrong - I was using MEF to resolve the type configuration classes, and I'd forgotten to configure the container properly for ASP.NET!
Richard
Monday, February 18, 2013 11:56:57 AM UTC
I recently came across a problem in an ASP.NET MVC Web Application where it needed to upload an image with some extra form fields.
please help me out.
Thanks in advance.
Friday, November 08, 2013 10:25:03 AM UTC
Hi Scott,

I'm a huge fan of asp.net 4.5 webforms model binding using data annotations.

Just 1 question:
Why is client side validation not supported out-of-the-box in webforms like in MVC?
Should we rely on third party libraries (DAValidation) or is it possible to port the goodness of Html.EnableClientValidation() to webforms ?

Regards,
Bart
Bart
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.