As you probably know I’m not neither an UX nor UI guy and in general, since the last few years, I’m trying to stay far away from something called “view”.
I’m involved in an existing Web-Forms project as consultant and sometimes as developer. The project has some years of active development and, although its success improves day by day, from the point of view of maintenance of some parts we have a problem: too much logic in the code-behind.
Following the rule of the CTO “business’s target at first” we have made a deal: each time we need to “re-touch” a use-case we will re-factorize each part involved.
The team, so far, has no experience with ASP.MVC and considering converting the whole project to ASP.MVC is not an option. As an example of the new work schema we have developed a small part of the application and deployed it in Windows Azure (webForm-MVP + WCF + NHibernate + SQL-Azure), that means, for us: no HttpSession, no VIEWSTATE… but this is another story. You can see the application here.
Our interpretation of MVP
In my opinion, the MVP pattern is one of the least clear of all patterns. This is so true that even the Passive View description has : “WORK-IN-PROGRESS: - this material is still under development”.
Our interpretation, for webForms, is simple: the Presenter is responsible to provide data to the View in the server side. The View know how render itself and what the client-side expect.
With this definition the Presenter, as most, can provide URL as strings but does not know anything about Request/Response, QueryString, Json, jQuery, HTML, javaScript, flash and any other technology involved in the View.
At this point what is the View, and which are its responsibilities, is simple: from the server-side code-behind to the HTML we are in the view and its responsibility is to render its self, to provide input data and, perhaps, to inform the presenter about some events.
The Ajax issue
Probably the most used solution, to resolve Ajax requests, in web-Forms, is through web-services (classic or WCF) or perhaps through some specific IHttpHandler… I worked with the team to accept MVP, and now ? Where is MVP if I have to separate the View’s needs, away from the Presenter ?
The other option is the usage of PageMethod with static methods marked as [WebMethod] and so on… could be usable but with some limitations.
The proof-of-concept
The intention is make the usage of Ajax as light as possible and of course to work with objects.
In the client-side I'll use what is normal today (including jQuery) and jQuery.form (no update-panel, no VIEWSTATE and so on). In the server side Json.NET.
To don’t make the story too much long here, have first a look to the show:
The use-case has two steps (two pages, apparently). There are 3 multiple-select; the first one is filled at page-load and the others are loaded through Ajax. The list of “Equipamiento” is loaded through Ajax. If you try to public (button “Publica”) and something is wrong the list of invalid values is showed (again through Ajax). You can go back and change any value. When you write something in e-mail input, it will check if the user exists and will show its address (“Calle”) through Ajax. Only when everything is OK the info is saved and you are redirected to a list. That’s all.
Instead of the VIEWSTATE I have used the view-state… not clear ? well… try to think about that the page has state in the browser ;-).
Ok… ok… I know… show me the code!!
The code behind
This is the code-behind (I mean the code behind the page in the server side).
public partial class ClasificadoCreate : View, IPublicationView
{
private PublicationPresenter presenter;
protected void Page_Load(object sender, EventArgs e)
{
presenter = new PublicationPresenter(this);
if(Request.IsAjaxRequest())
{
this.ResolveAjaxCallBack();
return;
}
if (!Page.IsPostBack)
{
presenter.InitView(Request.QueryString);
}
}
public void LoadModelos(int marca)
{
JsonResponse(presenter.GetModelos(marca));
}
public void LoadVersiones(int modelo)
{
JsonResponse(presenter.GetVersiones(modelo));
}
public void LoadEquipamiento(int version)
{
JsonResponse(presenter.GetEquipamiento(version));
}
public void LoadUsuario(string email)
{
JsonResponse(presenter.GetUsuario(email));
}
public IEnumerable<NameValueElement> Marcas
{
set
{
rptMarcas.DataSource = value;
rptMarcas.DataBind();
}
}
public void Publica(ClasificadoInfo clasificado)
{
JsonResponse(presenter.Register(clasificado));
}
}
Who calling those methods with parameters ?… especially the last-one where the parameter is the class ClasificadoInfo… and even more interesting if you think that its implementation is this:
public class ClasificadoInfo
{
public int MarcaId { get; set; }
public int ModeloId { get; set; }
public int VersionId { get; set; }
public IEnumerable<int> Equipamiento { get; set; }
public Usuario Usuario { get; set; }
public override string ToString()
{
var sb = new StringBuilder(300);
sb.AppendLine(string.Format("Marca={0},Modelo={1},Version={2}", MarcaId, ModeloId, VersionId));
sb.AppendLine("Equipamiento:");
if (Equipamiento != null)
{
foreach (int e in Equipamiento)
{
sb.Append(e).Append(',');
}
}
sb.AppendLine(string.Format("Usuario={0}", Usuario));
return sb.ToString();
}
}
public class Usuario
{
public string Email { get; set; }
public string Calle { get; set; }
public override string ToString()
{
return string.Format("Email={0},Calle={1}", Email, Calle);
}
}
As you can see that parameter is a class with a collection and a related class.
The code behind in the client side, changing a selected value, is:
<select name="ModeloId" multiple="multiple" id="listamodelos" size="5" onchange="return modelo_onchange()">
function modelo_onchange() {
var b = getFirstSelectedId("listamodelos");
JsonFromServerAction("LoadVersiones", { modelo: b }, function(a) {
replaceNameValueOptionsList("listaversiones", a)
});
replaceCategoriasEquipamiento(null)
}
The important part there is the function JsonFromServerAction whose implementation is:
function JsonFromServerAction(serverAction, params, onsuccess) {
var parameters = { LookupAction: serverAction };
$.getJSON(this.location.href, jQuery.extend(parameters, params), onsuccess);
}
As you can see LoadVersiones is the name of the method in the server-side-code-behind and modelo is the name of its parameter; pretty easy, clear and under convention.
Perhaps you are thinking that the code to post the entirely page inputs, to the server, will look more complicated… no? well… the answer is NO.
The client-side code is:
$(document).ready(function() {
$("#clasificado").ajaxForm({ url: this.location.href + "?LookupAction=Publica", dataType: 'json', success: showResponse })
});
To call the server-side code what you need is only "?LookupAction=Publica". To create the instance of the ClasificadoInfo parameter, of the method Publica, the convention to follow is simple. Each control name should match with a property name so:
<select name="VersionId" multiple="multiple" id="listaversiones" size="5" onchange="return version_onchange()">
mean that the selected value will be assigned to the property named VersionId and
e-mail:<input type="text" id="email" name="Usuario.Email" onchange="return usuario_onchange()" /><br />
Calle :<input type="text" id="calle" name="Usuario.Calle" /><br />
mean that the input value of email will be assigned to the property name Usuario.Email.
All the “magic” is done by the classes of HunabKu.Web (and obviously thanks to the others frameworks mentioned above).
If you want play a little bit, the code is available here or down load it.
Conclusion
Considering that we are talking about ~300 code-lines, I know that MonoRails and ASP.MVC are good and Web-Forms is evil… peeeero… la culpa no es siempre, y solo, del chancho.
Thanks for sharing your approach.
ReplyDeleteNow my concern: it looks like you are going through the whole page lifecycle (which is pretty heavyweight) just to return some data.
Against this alternative, I'd favor using Page Methods even though I don't like having static methods either.
I also considered using services and handlers, but as you said, I'd prefer to keep my view logic in the same place.
Another option I considered where you have better control of your results, is by using MVC to handle these ajax calls, but still keep webforms as your main view.
If you still think going through the page lifecycle is fine, then you might consider using the reduced lifecycle of ASP.NET 2.0 Callbacks.
What are your thoughts about this? Is it a concern for you also, and if not, what's the benefits that outweight the cons?
I don't understand exactly what you mean by: "whole page lifecycle just to return some data."
ReplyDeletein each GET I'm passing few parameters (the same you are doing using PageMethod).
in the POST I'm passing only the state of the form (as you probably are doing with PageMethod).
To hold every thing I'm using the browser memory (client-side) instead VIEWSTATE and instead HttpSession... enough scalable, no?
Right, I understand that you are not passing any viewstate or so on, but the page is still constructed as if you did a normal GET or POST. That means: all of the controls in your page will initialize as normally (they won't get the values nevertheless, because it's not a postback with viewstate), and the whole page lifecycle would commence.
ReplyDeleteI assume that after Page_Load, by calling the JsonResponse you cut short the response, and avoid the rendering phase, but nevertheless that is a lot of extra processing.
The difference might not be very noticeable in a very simple example, but as your page grows in complexity (and controls, and master pages), the penalty might become more apparent. Also considering that many of the ajax calls are done much more frequently than normal postback requests, which might call for the need of a scalable approach.
understood.
ReplyDeletebtw this approach is not to be used every where in a application but only in those cases where Ajax is a requirement and not an option.
How about creating one page that only handles ajax requests via webmethod that does not have any controls?
ReplyDeleteNo thanks... at that point a web/wcf service is what you really need.
ReplyDeleteHi Fabio,
ReplyDeleteWe've just released CTP1 of an MVP framework for Web Forms.
It's available at http://webformsmvp.com
Specifically related to your post, we have first class support for both HTTP handlers and Web Services. See: http://webformsmvp.codeplex.com/sourcecontrol/changeset/view/31034?projectName=webformsmvp#508608 and http://webformsmvp.codeplex.com/sourcecontrol/changeset/view/31034?projectName=webformsmvp#567027
I'd be interested to hear your thoughts on the approach we took.
- Tatham Oddie (@tathamoddie)
@tatham
ReplyDeleteInteresting; perhaps some more content about how it work would be useful.
btw I'm talking about ~300 code lines in 4 classes (the most complicated are those to translate the Request content to methods parameters).
la culpa es de quien le da de comer... Thanks Fabio... o mejor dicho gracias Fabio! apuesto que tu idioma nativo es el mismo que el mio, Español ;), llegué aqui a traves de un post del blog de Phil Haaack.
ReplyDelete