Tuesday, September 11, 2012

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

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

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.

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







© 2012 Scott Hanselman. All rights reserved.





DIGITAL JUICE

No comments:

Post a Comment

Thank's!