|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article discusses a strategy for automatic population of instance fields and properties on an ASP.NET page from parameters supplied via properties of the The ProblemIf you have parameterized ASP.NET pages - i.e. pages that modify their output on the basis of some input from the GET ( protected string FirstName{
get{ return Request.QueryString["FirstName"]; }
}
...but then you have to deal with the potential of protected static string IsNull(string test, string defaultValue){ return test==null ? defaultValue : test; } protected string FirstName_NullSafe{ get{ return IsNull(Request.QueryString["FirstName"],""); } } This approach works fine for non-string types too, you've just got to add your protected int CustomerID{ get{ object o=Request.QueryString["CustomerID"]; if (o==null) throw new ApplicationException("Customer ID is required to be passed"); else try{ return Convert.ToInt32(o,10); }catch(Exception err){ throw new ApplicationException("Invalid CustomerID", err); } } } } Yuk. What started out as a simple accessor is starting to grow a bit. Also that code's running every time we access It's probably about this point that we refactor the whole mess, and do all this work upfront in the private void Page_Load(object sender, System.EventArgs e) { string o; // This was the one we set a default for FirstName2 =IsNull(Request.QueryString["FirstName"], ""); // This one is required o =Request.QueryString["CustomerID"]; if (o==null) throw new ApplicationException("Customer ID is required to be passed"); else try{ CustomerID2 = Convert.ToInt32(o,10); }catch(Exception err){ throw new ApplicationException("Invalid CustomerID", err); } // This one's an enum (just to make life interesting) o =Request.QueryString["Gender"]; if (o==null) throw new ApplicationException("Gender is required"); else try{ Gender =(Gender)Enum.Parse(typeof(Gender), o, true); }catch(Exception err){ throw new ApplicationException("Invalid Gender", err); } } Now you've only got to do this a couple of times, and there's a clear pattern emerging. Whether you're populating fields or properties (i.e. ViewState wrappers), there's a couple of standard actions going on:
Now, I wouldn't be a proper coder if I wasn't lazy, and when it comes to this kind of boring repetitive stuff, I'm well lazy. So what's the alternative? The Solution: Declarative Parameter BindingWell, one solution would be to just 'mark up' the fields and properties we want loaded with appropriate metadata (specifying defaults, what key to bind against and in which collection) and just let some library code perform the actual work. Kind of like this: [WebParameter()] protected string FirstName; [WebParameter("Last_Name")] protected string LastName; [WebParameter(IsRequired=true)] protected int CustomerID; The optional constructor parameter just supplies the key to find the parameter in the All of a sudden, a great mass of code has condensed into a few attributes, which makes it easier to see at-a-glance what's going on, and simpler to maintain. All we need to do now is implement all that common logic we just trimmed out in some kind of helper class. This would use reflection to examine the (If you've never used reflection in .NET before, here's what you need to know to follow the code:
There're lots of tutorials about if you're interested.) The first step is to do exactly that, just loop through the properties and members on the class (here assigned to public static void SetValues(object target, System.Web.HttpRequest request) { System.Type type =target.GetType(); FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); MemberInfo[] members =new MemberInfo[fields.Length + properties.Length]; fields.CopyTo(members, 0); properties.CopyTo(members, fields.Length); for(int f=0;f<members.Length;f++) SetValue(members[f], target, request); } The only slightly confusing bit of this step is that we merge the Firstly, we exclude members that aren't marked with public static void SetValue(MemberInfo member, object target, System.Web.HttpRequest request) { WebParameterAttribute[] attribs; WebParameterAttribute attrib; TypeConverter converter; string paramValue; string paramName; object typedValue; bool usingDefault; try { attribs = (WebParameterAttribute[]) member.GetCustomAttributes(typeof(WebParameterAttribute), true); if(attribs!=null && attribs.Length>0) { // Just make sure we're not going after an indexed property if (member.MemberType==MemberTypes.Property) { ParameterInfo[] ps = ((PropertyInfo)member).GetIndexParameters(); if (ps!=null && ps.Length>0) throw new NotSupportedException("Cannot apply " + "WebParameterAttribute to indexed property"); } Now we get the various settings from the attribute, and the (string) parameter value itself. // There should only be one // WebParameterAttribute (it's a single-use attribute) attrib =attribs[0]; paramName =(attrib.ParameterName!=null) ? attrib.ParameterName : member.Name; paramValue =attrib.GetValue(paramName, request); Note that it's the attribute itself which supplies the actual parameter value we're after, making its own determination on which part of the If the attribute returns
// Handle default value assignment, if required usingDefault =false; if (paramValue==null) { if (attrib.DefaultValue!=null) { paramValue =attrib.DefaultValue; usingDefault =true; } else if (!attrib.IsRequired) return; // Just skip the member else throw new ApplicationException(String.Format("Missing " + "required parameter '{0}'", paramName)); } Now (finally), we can actually take our string and assign it to the member. We've got a couple of helper methods here just to make the code simpler (I won't reproduce the full source code here, but it's all in the example files):
I'm using a // Now assign the loaded value onto the member,
// using the relevant type converter
// Have to perform the assignment slightly
// differently for fields and properties
converter =
TypeDescriptor.GetConverter(GetMemberUnderlyingType(member));
if (converter==null ||
!converter.CanConvertFrom(paramValue.GetType()))
throw new
ApplicationException(String.Format("Could not" +
" convert from {0}", paramValue.GetType()));
try
{
typedValue =converter.ConvertFrom(paramValue);
SetMemberValue(member, target, typedValue);
}
catch
{
// We catch errors both from the type converter
// and from any problems in setting the field/property
// (eg property-set rules, security, readonly properties)
// If we're not already using the default, but there
// is one, and we're allowed to use it for invalid data,
// give it a go, otherwise just propagate the error
if (!usingDefault && attrib.IsDefaultUsedForInvalid
&& attrib.DefaultValue!=null)
{
typedValue =converter.ConvertFrom(attrib.DefaultValue);
SetMemberValue(member, target, typedValue);
}
else
throw;
}
}
Finally, we pick up on any exceptions arising from anywhere in the handling for this field, and wrap them in a standard error message }
catch(Exception err)
{
throw new ApplicationException("Property/field " +
"{0} could not be set from request - " + err.Message, err);
}
}
Phew. Now the obvious place for all this code would be in a base private void Page_Load(object sender, System.EventArgs e)
{
WebParameterAttribute.SetValues(this, Request);
}
Having to call it explicitly like this also saves on the overhead when you don't need it done, and means you get to choose when to bind: first time round; postback; every request - what you choose will depend on what your page does and whether you're binding to field or viewstate property accessors. Full ExampleHaving imported the relevant namespaces (and assemblies) into a page, all that's actually required to do all this is the very briefest of code (this is from the included example): public class WebParameterDemo : System.Web.UI.Page
{
[QueryParameter("txtFirstName")]
public string FirstName ="field default";
[QueryParameter("txtLastName", DefaultValue="attribute default")]
public string LastName{
get{ return (string)ViewState["LastName"]; }
set{ ViewState["LastName"]=value; }
}
[QueryParameter("txtCustomerID", DefaultValue="0")]
public int CustomerID;
private void Page_Load(object sender, System.EventArgs e)
{
WebParameterAttribute.SetValues(this, Request);
}
}
Benefits
Note also that it doesn't just have to be the Other parts of
Now, I was toying with using a similar approach to this for Console apps, but it turns out someone already did it (or something pretty similar), so if I've sold you on this, check out CommandLineOptions and ditch all that DrawbacksI like this approach a lot, it gives me more time for writing the complicated convoluted bits that get me out of bed in the morning. However it has one downside, and that's performance. Like anything based on reflection, its going to be slower than the equivalent direct call; I loop through a lot of unnecessary properties looking for those that are bound (and the That being said, reflection's only a bit slower than normal. Sure you wouldn't use it in your 3D rendering routines, but compared to the cost of your database access, wire-latency and other issues inherent in a web app and it's nothing. All that ASP.NET data binding you're using - that's reflection based. Still, if you're desperate, you could probably speed it all up by:
Finally, I should point out that in real life, I didn't hard-code the message into the exceptions like this - I stuck them in a resource file. Even though I don't intend globalizing my application, it makes it a lot easier for people re-using the assembly (including myself) to alter the messages to suit their needs without having to trawl through the source code. History2004-02-15 - First, much delayed, release. Appendix 1 - Why not to subclass System.Web.UI.PageIn the 'normal' world of OO programming, people believe in building little helper objects that can be used / aggregated within other classes to perform specific bits of functionality. However, when it comes to ASP.NET, there's way too much temptation to write your handy bit of common functionality as a base page class, with the obvious drawback that to use it you've got to derive from it, which means you're not deriving from something else. Additionally My advice is to have a few base page classes, sure, but keep it simple, and aggregate as much as possible of the more complex stuff as helper objects (even if they have to get passed a reference to their containing | ||||||||||||||||||||