Tuesday, April 3, 2012

Solving the ViewState code cluttering in ASP.net Web Forms

If you have worked on asp.net forms then you would have noticed the issue with viewstate from a maintenance point of view.  If we need to temporarily keep a value during post backs then one of the ways to do it is to store into page or control viewstate. Most of the rush jobs will introduce viewstate with magic strings (e.g. ViewState[“TestId”]) through out the page and soon you will see the a maintenance monster getting ready to overpower you. One of the ways to isolate this problem is to re-factor this code (view state magic strings) to properties.By this technique of limiting viewstate usage to a property we can achieve type safety and localize the issue.

But the code might look like this and its a good start as this approach gives some type safety.
     public long TestId
        {
            get
            {
                if (ViewState["TestId"] != null)
                {
                    return System.Convert.ToInt64(ViewState["TestId"]);
                }
                else
                {
                    return 0;
                }
            }
            set
            {
                ViewState["TestId"] = value;
            }
        }

While looking at a nicer way to solve this issue i came across with a code project blog which talks about creating reusable attributes.By applying this attribute based programming model we will be able to push data onto viewstate in a much clean way as below.

// new way of defining a viewstate property will be
[ViewStateProperty]
public long TestId { get; set; }

Though the real inspiration is from the above mentioned article, some customizations are done to support the generic use and explained implementation details in the following 5 steps.

Step 1. Define IViewStateProperty interface which can later provide extensibility and scoping (only those user control marked with this interface will have the feature of ViewStateProperty attribute)
 public interface IViewStateProperty    {    }

Step 2. Define ViewStateProperty attribute
   [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class ViewStateProperty : Attribute
    {
        public string ViewStateName { get; private set; }
        public object DefaultValue { get; set; }

        public ViewStateProperty()
        {
            this.ViewStateName = string.Empty;
        }

        public ViewStateProperty(object defaultValue)
        {
            this.DefaultValue = defaultValue;
        }
    }  

Step 3. Define ViewStatePageBase Custom UI Page Base class
/// <summary>/// DEVNOTE: ViewStateProperty instances that are declared as 'private' may not //// be found on reflection (if the application is running in medium trust. E.g. /// when you are running on a shared hosting environment)/// </summary>
  public class ViewStatePageBase : System.Web.UI.Page
    {
        protected override void OnPreLoad(EventArgs e)
        {
            base.OnPreLoad(e);

            this.DoLoadViewStateProperties(this);
            this.LoadViewStatePropertiesRecursive(this.Controls);
        }

        protected override void OnPreRenderComplete(EventArgs e)
        {
            base.OnPreRenderComplete(e);

            this.DoSaveViewStateProperties(this);
            this.SaveViewStatePropertiesRecursive(this.Controls);
        }

        private void LoadViewStatePropertiesRecursive(ControlCollection controls)
        {
            foreach (Control ctrl in controls)
            {
                if (ctrl is IViewStateProperty)
                {
                    this.DoLoadViewStateProperties(ctrl);
                }
                LoadViewStatePropertiesRecursive(ctrl.Controls);
            }
        }

        private void SaveViewStatePropertiesRecursive(ControlCollection controls)
        {
            foreach (Control ctrl in controls)
            {
                if (ctrl is IViewStateProperty)
                {
                    this.DoSaveViewStateProperties(ctrl);
                }
                this.SaveViewStatePropertiesRecursive(ctrl.Controls);
            }
        }

        private void DoLoadViewStateProperties(Control ctrl)
        {
            var properties = ctrl.GetType().GetProperties(BindingFlags.Public 
                           | BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(prop => Attribute.IsDefined(prop, typeof(ViewStateProperty), true))
                .Select(p => new ViewStatePropertyInfo()
                {
                    ViewStateName = ((ViewStatePropertyp.GetCustomAttributes (typeof(ViewStateProperty), true)
                                             .FirstOrDefault()).ViewStateName,
                    PropertyInfo = p
                });

            foreach (var property in properties)
            {
                var localName = String.Format("{0}_{1}"
                                                , ctrl.ClientID
                                                , String.IsNullOrEmpty(property.ViewStateName)
                                                    ? property.PropertyInfo.Name
                                                    : property.ViewStateName); 
                if (ViewState[localName] != null)
                {
                    property.PropertyInfo.SetValue(ctrl, ViewState[localName], null);
                }
            }
        }

        private void DoSaveViewStateProperties(Control ctrl)
        {
            var properties = ctrl.GetType().GetProperties(BindingFlags.Public 
                                        | BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(prop => Attribute.IsDefined(prop, typeof(ViewStateProperty), true))
                .Select(p => new ViewStatePropertyInfo()
                {
                    ViewStateName = ((ViewStateProperty)p.GetCustomAttributes(typeof(ViewStateProperty), true)
                                         .FirstOrDefault()).ViewStateName,
                    PropertyInfo = p
                });

            foreach (var property in properties)
            {
                var localName = String.Format("{0}_{1}"
                                                , ctrl.ClientID
                                                , String.IsNullOrEmpty(property.ViewStateName)
                                                    ? property.PropertyInfo.Name
                                                    : property.ViewStateName);
                ViewState[localName] = property.PropertyInfo.GetValue(ctrl, null);
            }
        }

        private struct ViewStatePropertyInfo
        {
            internal string ViewStateName { get; set; }
            internal PropertyInfo PropertyInfo { get; set; }
        }
    }
Step 4. Apply layer super type pattern to pages (aspx.cs) and ensure they inherit from custom base class (ViewStatePageBase) instead of System.Web.UI.Page.
e.g. A Test Page will inherit from a custom ViestatePageBase as follows
public partial class TestPage : ViewStatePageBase

Step 5. To bring user controls to this equation try inheriting from a base class (UserControlBase) and implement with the IViewStateProperty  

public class UserControlBase : System.Web.UI.UserControl, IViewStateProperty

i.e. TestUserControl.ascx.cs will be as defined follows
public class TestUserControl : UserControlBase

Now you are all set to decorate properties with [ViewStateProperty] attribute and use code as below
[ViewStateProperty]
public long TestId { getset; }