Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

FormVarsToObject - a quick way to collect form input values


:P
On this page:

Here's a useful little utility routine that can come in quite handy ASP.NET and AJAX style scnenarios. How many times have you written code like this:

CustomerEntity entity = new CustomerEntity();
 
entity.FirstName = this.txtFirstName.Text;
entity.LastName = this.txtLastName.Text;
entity.BillingRate = decimal.Parse(this.txtBillingRate.Text);
...

Ok, I realize there are often other ways that you can handle this sort of data assignment scenario, whether you use ASP.NET's internal data controls like a FormView or whether you use a custom databinding mechanism like my wwDataBinder (which I use almost everywhere).

But sometimes there are scenarios where databinding is not required or - more common recently - where you retrieve data from an AJAX request in a callback and all you really need to do is get the damn values into a simple object. AJAX requests in particular are interesting because you have a choice in many cases on whether you pass data back as JSON formatted from the client, or whether you simply pass no parameters and instead let the POST data be passed back to the server.

Frankly I prefer the latter, because it's actually easier to peel data out in the ASP.NET backend from formvars directly rather than doing it on the client and handle object creation and parsing types on the client. There's little benefit in that respect. Most AJAX frameworks support automatically sending form post data so that's an easy way to get data to the server. The other related issue is type conversion which can be tedious, mainly because you typically have to do verification of the manual assignment and conversion.

Using a Simple Routine - FormVarsToObject

So... for cases like this I have a routine that helps make this process easier. The idea is simple: The routine takes form variables and maps them to the properties of an object. Actually it's rather then other way around: An object looks through it's list of propeties and tries to populate itself from Form variables with the same name or that follow a specific naming pattern.

There's an implied assumption in this scenario: FormVariables and fieldnames have to match or match with a specific prefix. I have for the longest time used a naming scheme that matches object/data keys anyway and that' s a good idea in general. So you can specify an object to populate and an optional prefix that is used for form variables, such as txt for example. Another option is to get any errors returned as error messages.

To use the method is dirt simple - you specify and object and the prefix(es) you want to look at:

if (!wwWebUtils.FormVarsToObject(this.Customer, "txt|chk|lst")
{
    this.ErrorDisplay.ShowError("Invalid data in the form");
}
// ... good to go

The routine goes out grabs vars and assign them to whatever object you pass in. The method returns true or false - if any sort of bindback error occurs it returns false.

You can get more error information by passing in a Dictionary<string,string> which returns a collection of errors:

protected void btnSubmit_Click(object sender, EventArgs e)        
{
 
    Dictionary<string,string> errorList = new Dictionary<string,string>();
    if (!wwWebUtils.FormVarsToObject(this.Customer, "txt|chk", errorList))
    {
        string errors = "";
        foreach (KeyValuePair<string, string> kv in errorList)
        {
            errors += "<li>" + kv.Key + ": " + kv.Value + "</li>\r\n" ;
        }
 
        this.ErrorDisplay.ShowError(Errors);
        return;
    }
 
    this.ErrorDisplay.ShowMessage("Customer saved");
}

You can of course still use validatators etc. to validate errors on the page directly.

Keep in mind that naming is key. Everything that you'd need to unbind in this simple fashion needs to have a txt or chk or lst prefix (or whatever you specify) and the names need to match exactly.

So here's what this FormVarsToObject helper method looks like (plus some support routines shown at the end of the post):

 
/// <summary>
/// Routine that can be used read Form Variables into an object if the 
/// object name and form variable names match either exactly or with a specific
/// prefix.
/// 
/// The method loops through all *public* members of the object and tries to 
/// find a matching form variable by the same name in Request.Form.
/// 
/// The routine returns false if any value failed to parse (ie. invalid
/// formatting etc.). However parsing is not aborted on errors so all
/// other convertable values are set on the object.
/// 
/// You can pass in a Dictionary<string,string> for the Errors parameter
/// to optionally retrieve unbinding errors. The dictionary key holds the
/// simple form varname for the field (ie. txtName), the value the actual
/// exception error message
/// </summary>
/// <remarks>
/// This method can have unexpected side-effects if multiple naming
/// containers share common variable names. This routine is not recommended
/// for those types of pages.
/// </remarks>
/// <param name="Target"></param>
/// <param name="FormVarPrefix">empty or one or more prefixes spearated by |</param>
/// <returns>true or false if an unbinding error occurs</returns>
public static bool FormVarsToObject(object target, string formvarPrefixes, Dictionary<string,string> errors)
{            
    bool isError = false;
    List<string> ErrorList = new List<string>();
 
    if (formvarPrefixes == null)
        formvarPrefixes = "";
 
    if (HttpContext.Current == null)
        throw new InvalidOperationException("FormVarsToObject can only be called from a Web Request");
 
    HttpRequest Request = HttpContext.Current.Request;            
 
    // *** try to get a generic reference to a page for recursive find control
    // *** This value will be null if not dealing with a page (ie. in JSON Web Service)
    Page page = HttpContext.Current.Handler as Page;
 
    MemberInfo[] miT = target.GetType().FindMembers(  
        MemberTypes.Field | MemberTypes.Property,
        BindingFlags.Public | BindingFlags.Instance,
        null, null);
 
    // *** Look through all prefixes separated by |
    string[] prefixes = formvarPrefixes.Split('|');
 
    foreach (string prefix in prefixes)
    {
 
        // *** Loop through all members of the Object
        foreach (MemberInfo Field in miT)
        {
            string Name = Field.Name;
 
            FieldInfo fi = null;
            PropertyInfo pi = null;
            Type FieldType = null;
 
            if (Field.MemberType == MemberTypes.Field)
            {
                fi = (FieldInfo)Field;
                FieldType = fi.FieldType;
            }
            else
            {
                pi = (PropertyInfo)Field;
                FieldType = pi.PropertyType;
            }
 
            // *** Lookup key will be field plus the prefix
            string formvarKey = prefix + Name;
 
            // *** Try a simple lookup at the root first
            string strValue = Request.Form[prefix + Name];
 
 
            // *** if not found try to find the control and then
            // *** use its UniqueID for lookup instead
            if (strValue == null && page != null)
            {
                Control ctl = wwWebUtils.FindControlRecursive(page, formvarKey);
                if (ctl != null)
                    strValue = Request.Form[ctl.UniqueID];
            }
 
            // *** Bool values and checkboxes might require special handling
            if (strValue == null)
            {
                // *** Must handle checkboxes/radios
                if (FieldType is bool)
                    strValue = "false";
                else
                    continue;
            }
 
            try
            {
                // *** Convert the value to it target type
                object Value = wwUtils.StringToTypedValue(strValue, FieldType);
 
                // *** Assign it to the object property/field
                if (Field.MemberType == MemberTypes.Field)
                    fi.SetValue(target, Value);
                else
                    pi.SetValue(target, Value, null);
            }
            catch (Exception ex)
            {
                isError = true;
                if (errors != null)
                    errors.Add(Field.Name, ex.Message);
            }
        }
    }
 
    return !isError;
}
 
public static bool FormVarsToObject(object Target, string FormVarPrefix)
{
    return FormVarsToObject(Target, FormVarPrefix, null);
}

As you can see the code uses Reflection to parse through the object and it's one dimensional - it only works on top level properties. If you have nested properties those are simply ignored. You can call the routine multiple times for each of the subobjects as necessary.

Errors are flagged only during unbinding. If a value doesn't exist in the POST data it's just not assigned. Errors messages are optionally captured and passed back in a Dictionary if you pass one in. This allows you display error information or take action on the errors. By itself this provides rudimentary error display functionality, but you can double this up with validators for more robust checks.

There are two helper methods used for the above code. The first is FindControlRecursive() that as the name implies can find controls deep in a nested page hierarchy by looking into containers. The code tries to read a form variable directly first for best performance and if that fails tries to use FindControlRecursive to try and figure out which control holds the ID we're looking for. If a control is found its UniqueID is used as the Request Lookup key. This is necessary so that the routine can work in nested containers.

The second is a generic conversion routine that takes a string value and turns it into a specific type StringToTypedValue(). These functions are listed at the bottom of this post.

I won't suggest that this is a general way to do data binding in your applications. But I've found this routine very handy in quick and dirty forms that I set up for demos or when I do hands on demonstrations. Nobody wants to sit and type manual control assignments. In other places like in AJAX apps though this actually is even more useful because there's really no official way to parse data from input form data, so there this actually makes a lot more sense and this is where I've used this routine quite a bit (specially page level callbacks).

For more sophisticated scenarios wwDataBinder is my personal choice for databinding on forms. That control provides more control for each individual databound item and lets you pretty much bind anything to anything not just objects.

The same sort of thing can be applied to reading data back into a DataRow:

/// <summary>
/// Routine that retrieves form variables for each row in a dataset that match
/// the fieldname or the field name with a prefix.
/// The routine returns false if any value failed to parse (ie. invalid
/// formatting etc.). However parsing is not aborted on errors so all
/// other convertable values are set on the object.
/// 
/// You can pass in a Dictionary<string,string> for the Errors parameter
/// to optionally retrieve unbinding errors. The dictionary key holds the
/// simple form varname for the field (ie. txtName), the value the actual
/// exception error message
/// <seealso>Class wwWebUtils</seealso>
/// </summary>
/// <param name="loRow">
/// A DataRow object to load up with values from the Request.Form[] collection.
/// </param>
/// <param name="Prefix">
/// Optional prefix of form vars. For example, "txtCompany" has a "txt" prefix 
/// to map to the "Company" field. Specify multiple prefixes and separate with |
/// Leave blank or null for no prefix.
/// </param>
/// <param name="errors">
/// An optional Dictionary that returns an error list. Dictionary is
/// has a string key that is the name of the field and a value that describes the error.
/// Errors are binding errors only.
/// </param>
public static bool FormVarsToDataRow(DataRow dataRow, string formvarPrefixes, Dictionary<string,string> errors)
{
    bool isError = false;
 
    if (HttpContext.Current == null)
        throw new InvalidOperationException("FormVarsToObject can only be called from a Web Request");
 
    HttpRequest Request = HttpContext.Current.Request;
 
    // *** try to get a generic reference to a page for recursive find control
    // *** This value will be null if not dealing with a page (ie. in JSON Web Service)
    Page page = HttpContext.Current.Handler as Page;
 
 
    if (formvarPrefixes == null)
        formvarPrefixes = "";
 
    DataColumnCollection columns = dataRow.Table.Columns;
 
                // *** Look through all prefixes separated by |
    string[] prefixes = formvarPrefixes.Split('|');
 
    foreach (string prefix in prefixes)
    {
        foreach (DataColumn column in columns)
        {
            string Name = column.ColumnName;
 
            // *** Lookup key will be field plus the prefix
            string formvarKey = prefix + Name;
 
            // *** Try a simple lookup at the root first
            string strValue = Request.Form[prefix + Name];
 
            // *** if not found try to find the control and then
            // *** use its UniqueID for lookup instead
            if (strValue == null && page != null)
            {
                Control ctl = wwWebUtils.FindControlRecursive(page, formvarKey);
                if (ctl != null)
                    strValue = Request.Form[ctl.UniqueID];
            }
 
            // *** Bool values and checkboxes might require special handling
            if (strValue == null)
            {
                // *** Must handle checkboxes/radios
                if (column.DataType == typeof(Boolean))
                    strValue = "false";
                else
                    continue;
            }
 
            try
            {
                object value = wwUtils.StringToTypedValue(strValue, column.DataType);
                dataRow[Name] = value;
            }
            catch (Exception ex)
            {
                isError = true;
                if (errors != null)
                    errors.Add(Name, ex.Message);
            }
 
        }
    }
 
    return !isError;
}

Same idea as with the object except here the data gets read into the data row. Note that reading into a data row is actually more efficient than reading into object properties because there's no Reflection involved.

Finally, here are the two dependencies for the code above.

/// <summary>

/// Finds a Control recursively. Note finds the first match and exits
/// </summary>
/// <param name="ContainerCtl">The top level container to start searching from</param>
/// <param name="IdToFind">The ID of the control to find</param>
/// <returns></returns>
public static Control FindControlRecursive(Control Root, string Id)
{
    return FindControlRecursive(Root, Id, false);
}
 
/// <summary>
/// Finds a Control recursively. Note finds the first match and exits
/// </summary>
/// <param name="ContainerCtl">The top level container to start searching from</param>
/// <param name="IdToFind">The ID of the control to find</param>
/// <param name="AlwaysUseFindControl">If true uses FindControl to check for hte primary Id which is slower, but finds dynamically generated control ids inside of INamingContainers</param>
/// <returns></returns>
public static Control FindControlRecursive(Control Root, string Id, bool AlwaysUseFindControl)
{
    if (AlwaysUseFindControl)
    {
        Control ctl = Root.FindControl(Id);
        if (ctl != null)
            return ctl;
    }
    else
    {
        if (Root.ID == Id)
            return Root;
    }
 
    foreach (Control Ctl in Root.Controls)
    {
        Control FoundCtl = FindControlRecursive(Ctl, Id, AlwaysUseFindControl);
        if (FoundCtl != null)
            return FoundCtl;
    }
 
    return null;
}
/// <summary>
/// Turns a string into a typed value. Useful for auto-conversion routines
/// like form variable or XML parsers.
/// <seealso>Class wwUtils</seealso>
/// </summary>
/// <param name="SourceString">
/// The string to convert from
/// </param>
/// <param name="TargetType">
/// The type to convert to
/// </param>
/// <param name="Culture">
/// Culture used for numeric and datetime values.
/// </param>
/// <returns>object. Throws exception if it cannot be converted.</returns>
public static object StringToTypedValue(string SourceString, Type TargetType, CultureInfo Culture)
{
    object Result = null;
 
    if (TargetType == typeof(string))
        Result = SourceString;
    else if (TargetType == typeof(int))
        Result = int.Parse(SourceString, NumberStyles.Integer, Culture.NumberFormat);
    else if (TargetType == typeof(byte))
        Result = Convert.ToByte(SourceString);
    else if (TargetType == typeof(decimal))
        Result = Decimal.Parse(SourceString, NumberStyles.Any, Culture.NumberFormat);
    else if (TargetType == typeof(double))
        Result = Double.Parse(SourceString, NumberStyles.Any, Culture.NumberFormat);
    else if (TargetType == typeof(bool))
    {
        if (SourceString.ToLower() == "true" || SourceString.ToLower() == "on" || SourceString == "1")
            Result = true;
        else
            Result = false;
    }
    else if (TargetType == typeof(DateTime))
        Result = Convert.ToDateTime(SourceString, Culture.DateTimeFormat);
    else if (TargetType.IsEnum)
        Result = Enum.Parse(TargetType, SourceString);
    else
    {
        System.ComponentModel.TypeConverter converter = System.ComponentModel.TypeDescriptor.GetConverter(TargetType);
        if (converter != null && converter.CanConvertFrom(typeof(string)))
            Result = converter.ConvertFromString(null, Culture, SourceString);
        else
        {
            System.Diagnostics.Debug.Assert(false, "Type Conversion not handled in StringToTypedValue for " +
                                            TargetType.Name + " " + SourceString);
            throw (new ApplicationException("Type Conversion not handled in StringToTypedValue"));
        }
    }
 
    return Result;
}
 
/// <summary>
/// Turns a string into a typed value. Useful for auto-conversion routines
/// like form variable or XML parsers.
/// </summary>
/// <param name="SourceString">The input string to convert</param>
/// <param name="TargetType">The Type to convert it to</param>
/// <returns>object reference. Throws Exception if type can not be converted</returns>
public static object StringToTypedValue(string SourceString, Type TargetType)
{
    return StringToTypedValue(SourceString, TargetType, CultureInfo.CurrentCulture);
}

Anyway, thought this might be a useful little routine for some of you.

Posted in ASP.NET  DataBinding  

The Voices of Reason


 

espinete
September 07, 2007

# re: FormVarsToObject - a quick way to collect form input values

Hi, mister, it's great.

A question, what's happening about performance using your code ? good performance using reflection for get values of a form ??

Thanks in advance. Greetings.

Rick Strahl
September 07, 2007

# re: FormVarsToObject - a quick way to collect form input values

Well, as you point out it uses Reflection there's defnitely some overhead involved, especially with objects. But this is relative. I ran some stress testing on continous posts on a form with 15 fields and compared against a GET of a form and the difference between the two was barely detectable in the test. It's data unbinding so this isn't very likely to be a high traffic situation anyway and it's not going to run in a loop. So it's probably acceptable in terms of the effect it has on performance.

Also remember that ASP.NET's native databinding also has to use Reflection (if you bind to objects that is) - pretty much any approach to databinding that isn't based on pre-generated code or hardcoding the values will need to use a similar mechanism.

Jeff
September 08, 2007

# re: FormVarsToObject - a quick way to collect form input values

It's good you made the prefix optional because Hungarian has gone the way of the dinosaurs. Now if you had a database with the same names as the object's properties, you could do a similar thing with it and life would become too easy.

Thanks.

Rick Strahl
September 08, 2007

# re: FormVarsToObject - a quick way to collect form input values

@Jeff - agreed about hungarian, but I still use it out of habit on HTML forms. One of the reasons is that in past pre-ASP.NET frameworks I extensively relied a FormVarsToObject() behavior for unbinding functionality and using the prefixes made life somewhat easier when searching through forms.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024