Saturday, March 29, 2008

Web user controls design pattern and data binding part 1

A lot of people are using user controls in asp because they are very easy to use, have design time support and so on.
Anyway, because this controls are like a web page that you can add it in every page you want, you tend to do some mistakes and forget about user control design line. Most common mistakes are:
  1. put logic functionality into control
  2. forget about implementing events, and use basic control events like button click, dropdown SelectedIndexChanged, etc.
  3. not treat it as a control, implementing a data source, and a data bind mechanism
You will get into a lot of trouble , especially when page will be very complex and of course you will loose the flow of this page, spend many hours of debug to patch problems of logic and so on.

Whats the plan?
The purpose of this article is to design a user control for editing a user details object, save data, cancel editing or deleting the user object.
You can download web site here (use Save target as) or you can read on.
Notice: this is a website with VS 2008 and framework 3.5. It'll not work on VS 2005
  • Data Model
Let's say we have a user object defined as below
public enum Gender
{
Female = 0,
Male = 1
}
[Serializable()]
public class User
{
public int UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
public Gender UserGender { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public int Age { get; set; }

public User()
{
}
}
Notice that we use an enum for user gender.

Well we will first design the control (let's say it's called UserDetails.ascs). I don't think it's a good idea to put code below, because it's just some long text with asp tags, so we got to code behind to implement some guidelines or this user control.
Well in my opinion a control should have:
    • a datasource containing data to be displayed
    • a binding method that fill control with fed data
    • an unbinding method so we can grab user changes back to datasource
    • some custom events for Save, Cancel, Delete to handle this actions in main page (not in user control as I said above)
Well, let's go to work.
  1. Implement a datasource:
public User DataSource
{
get
{
if (ViewState[this.ClientID + "USER"] != null)
{
return (User)ViewState[this.ClientID + "USER"];
}
else
{
return null;
}
}
set
{
ViewState[this.ClientID + "USER"] = value;
}
}
2. Implement a binding method

/// <summary>
/// Populate controls with user values
/// </summary>
public override void DataBind()
{
//call base method
base.DataBind();

#region Fill controls

//populate dropdown with options for gender
ddlGender.Items.Clear();
foreach (string enumName in Enum.GetNames(typeof(Gender)))
{
int enumValue = Convert.ToInt32( Enum.Parse(typeof(Gender), enumName));
ListItem li = new ListItem(enumName, enumValue.ToString());
ddlGender.Items.Add(li);
}

#endregion

#region Bind Controls

this.txtFirstName.Text = DataSource.FirstName;
this.txtMiddleName.Text = DataSource.MiddleName;
this.txtLastName.Text = DataSource.LastName;
this.txtAge.Text = DataSource.Age.ToString();
this.ddlGender.SelectedValue = ((int)DataSource.UserGender).ToString();
this.txtAddress.Text = DataSource.Address;
this.txtPhone.Text = DataSource.Phone;

#endregion
}
As you can see we set control values with our DataSource(User object) values in here so we decide when we want some binding in the main page.

3. Implement an Unbind method
You can see here we set the control values changed by the user to our DataSource and we decide in the main page if we want to do the saving(save the control changed data source) to some other object, database, etc.
Notice: you can make this method public an call it from the main page also.

/// <summary>
/// Binds changed values from controls to DataSource(user object)
/// </summary>
private void UnBind()
{
DataSource.FirstName = this.txtFirstName.Text;
DataSource.MiddleName = this.txtMiddleName.Text;
DataSource.LastName = this.txtLastName.Text;
DataSource.Age = Int32.Parse( this.txtAge.Text);
DataSource.UserGender = (Gender)Int32.Parse(this.ddlGender.SelectedValue);
DataSource.Address = this.txtAddress.Text;
DataSource.Phone = this.txtPhone.Text;

//add user Id as command argument if needed in parent page
this.btnCancel.CommandArgument = DataSource.UserId.ToString();
this.btnDeleteUser.CommandArgument = DataSource.UserId.ToString();
this.btnSaveUser.CommandArgument = DataSource.UserId.ToString();
}
4. Implement some custom events.
As I said before this custom events will help you do the job in the parent page, where control is placed so let's implement some basic events this controls needs:
Declaring events is very simple:
public event EventHandler SaveUser;
public event EventHandler DeleteUser;
public event EventHandler CancelEdit;
and implementing them is also not hard to do:

protected virtual void OnSaveUser(object sender, EventArgs e)
{
if (this.SaveUser != null)
{
SaveUser(sender, e);
}
}

protected virtual void OnDeleteUser(object sender, EventArgs e)
{
if (this.DeleteUser != null)
{
DeleteUser(sender, e);
}
}

protected virtual void OnCancelEdit(object sender, EventArgs e)
{
if (this.CancelEdit != null)
{
CancelEdit(sender, e);
}
}
Now we just have to call this defined events on button click events (for btnSave, btnDelete, btnCancel):



protected void btnSaveUser_Click(object sender, EventArgs e)
{
//bind back values
this.UnBind();
//we treat saving event in the parent page not in control
this.OnSaveUser(sender, e);
}
protected void btnDeleteUser_Click(object sender, EventArgs e)
{
//we treat saving event in the parent page not in control
this.OnDeleteUser(sender, e);
}
protected void btnCancel_Click(object sender, EventArgs e)
{
//we treat saving event in the parent page not in control
this.OnCancelEdit(sender, e);
}
Please notice in Save click method I call Unbind to set DataSource with values user canged in textboxes, dropdowns, etc.

As you can see this is not hard to do, sometimes missing small details makes your pages hard to follow by others, or event by yourself after a while.

Using the control

Using the control in the age will be very easy job for us now. In my code I also added some status labels so you can see what's happening and some status values for User properties on the page.
I'll skip asp markup and go to the code behind.

Steps to do here:

1. Declare user control events in OnInit method:

protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.UserDetails1.SaveUser += new EventHandler(UserDetails1_SaveUser);
this.UserDetails1.DeleteUser += new EventHandler(UserDetails1_DeleteUser);
this.UserDetails1.CancelEdit += new EventHandler(UserDetails1_CancelEdit);
}
2. DataSource and DataBind
We first have to initialize the control with some data, the do the binding.
Suppose we have and object in the main page called SelectedUser, we obtained form database or whatever. All we have to to is:


protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
this.UserDetails1.DataSource = SelectedUser;
this.UserDetails1.DataBind();
}
You notice here I put this code OnPreRender method, this is because this is executed after all page user defined events like Save, Cancel, Delete. I did this because I don't want to call this code twice : page load and then user handled event. This way we only bind control once after all custom logic is executed.

3. Handle events in page.
We have only one step, decide what to do when user save, delete or cancel changes to this User control.



void UserDetails1_CancelEdit(object sender, EventArgs e)
{
//we cancel changes by databind control again with initial values
this.UserDetails1.DataSource = SelectedUser;
this.UserDetails1.DataBind();

lblAction.Text = "Cancel Edit";
}

void UserDetails1_DeleteUser(object sender, EventArgs e)
{
//do you delete logic here
lblAction.Text = "Delete User";
}

void UserDetails1_SaveUser(object sender, EventArgs e)
{
//set changed user value in user control to page User object
this.SelectedUser = this.UserDetails1.DataSource;
lblAction.Text = "Save User";
}
Well, I hope you enjoyed this reading.
kick it on DotNetKicks.com

2 comments:

Girish Girigowda said...

The link to the code does not work.

ASP said...

Should work now.