Tuesday 6 April 2010

Dynamic User Controls – Part 4 – Data Bound User Controls

We have shown in the previous post how to dynamically add a standard ASP.NET control to a page. Now we are going to look at how to make dynamically add user controls to the page and also make them databound. First of all we need to look at our entities. We have a very simple Person class, which has a couple of properties.
 public class Person
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}

As you can see it is very basic. The user control we will be using, models this in front end code.
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="PersonControl.ascx.cs" Inherits="DynamicUserControls.Web.UserControls.PersonControl" %>
<table>
<tr>
<td>First Name</td>
<td><asp:TextBox ID="txtFirstName" runat="server" Width="300px"></asp:TextBox></td>
</tr>
<tr>
<td>Last Name</td>
<td><asp:TextBox ID="txtLastName" runat="server" Width="300px"></asp:TextBox></td>
</tr>
</table>

The textboxes just represent the the strings in the class. We also have an interface which we will implement in the user control.
public interface IPersonControl
{
Person Data { get; set; }
void DataBind();
}

To implement this in our user control we use the following code
public partial class PersonControl : UserControl, IPersonControl

If you are using ReSharper you can press ALT+Enter to implement the members, although I am fairly sure Visual Studio 2008 and above does this as well. The codebehind of the user control looks like this
public partial class PersonControl : UserControl, IPersonControl
{
private Person _data;

public override void DataBind()
{
if (Data != null)
{
txtFirstName.Text = Data.FirstName;
txtLastName.Text = Data.LastName;

_data = null;
}

base.DataBind();
}


public Person Data
{
get { return _data ?? (
_data = new Person
{
FirstName = txtFirstName.Text,
LastName = txtLastName.Text
}); }
set { _data = value; }
}
}

We have a new property called Data which allows us to get and set the information in the control. We also have a DataBind method which is fired when the control being databound.

The Data property when set, sets a private variable called _data which we will come back to later. When we DataBind our control, we set the values of the textboxes to the appropriate values of the class and we null the _data variable so that each time when we get Data property, we check to see if _data is null i.e. has been databound. We can then assign the values of the textboxes to a new instance of the class and return it. In the code above, I am using a null-coalescing operator (??) to shorten the code a bit.

That is all we need to do with the user control for the moment. We will now swap back to the Default.aspx page and add a new tab to the page.
<cc1:TabPanel ID="tabPeople" runat="server" HeaderText="Person User Control Demo">
<ContentTemplate>
<asp:Repeater ID="rptPeople" runat="server">
<ItemTemplate>
<asp:PlaceHolder ID="phPerson" runat="server"></asp:PlaceHolder>
</ItemTemplate>
</asp:Repeater>
<asp:Button ID="btnAddPerson" runat="server" Text="Add a person" CausesValidation="false" OnClick="AddPerson" />
</ContentTemplate>
</cc1:TabPanel>

As you can see it is very similar to the tab we created for the textbox example in the previous post. We will also create a new page property in the code behind file called CurrentPersonList
List<Person> CurrentPersonList
{
get
{
var items = new List<Person>();

foreach(RepeaterItem item in rptPeople.Items)
{
var control = (PersonControl) item.FindControl("phPerson").Controls[0];
items.Add(control.Data);
}
return items;
}
}

We can reference our Data property in our control by casting the control we access in the placeholder as a PersonControl. And because the Data property is a Person class we can add it to our generic list.

Adding a new control is a case of getting the current list, adding a new Person class, set the data source of the repeater to the new list and the binding the list again.
protected void AddPerson(object sender, EventArgs e)
{
var currentData = CurrentPersonList;
currentData.Add(new Person());
rptPeople.DataSource = currentData;
rptPeople.DataBind();
}

As you can see we take a copy of the current data, add a new one and rebind the repeater. Now we need to override the ItemCreated event of the repeater and change it so that it loads a user control with the correct data.
private void RptPeopleItemCreated(object sender, RepeaterItemEventArgs e)
{
var data = (Person) e.Item.DataItem;
if(!Equals(data,default(Person)))
{
var placeholder = (PlaceHolder) e.Item.FindControl("phPerson");
var control = (PersonControl) LoadControl("~/UserControls/PersonControl.ascx");
control.Data = data;
placeholder.Controls.Add(control);
}
else
{
e.Item.Load += PersonItemLoad;
}
}

First we cast the DataItem as a Person object and then we check to see if its a blank object i.e. default. If its not a blank object, we need to load the control and set its Data property. So we need to get the placeholder control in the ItemTemplate. We need to then load an instance of our user control and set the Data property with the casted DataItem. We add the control to the Controls collection of the placeholder. On the other hand if the object is blank we just want to load the control with no Data property set. So we use the Item.Load event to achieve this.
var literal = (RepeaterItem)sender;
var placeHolder = literal.FindControl("phPerson");
var control = (PersonControl)LoadControl("~/UserControls/PersonControl.ascx");
placeHolder.Controls.Add(control);

So its very similar to the ItemCreated event. We can now run the page and it will add a user control to the page when you click the button. That’s the first part of the story, the second part is being able to remove the item from the page. There is no point in being able to add controls if you cannot remove them-

To do this we are going to add a LinkButton to the user control so that it now looks like this
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="PersonControl.ascx.cs" Inherits="DynamicUserControls.Web.UserControls.PersonControl" %>
<table>
<tr>
<td>First Name</td>
<td><asp:TextBox ID="txtFirstName" runat="server" Width="300px"></asp:TextBox></td>
</tr>
<tr>
<td>Last Name</td>
<td><asp:TextBox ID="txtLastName" runat="server" Width="300px"></asp:TextBox>&nbsp;<asp:LinkButton ID="lnkRemove" runat="server" OnClick="RemoveItem" Text="Remove this item"></asp:LinkButton></td>
</tr>
</table>

With the new link button we have a handler that will raise an event which we can handle in our repeater. We will send the command “Remove” to whatever handler is listening.


     protected void RemoveItem(object sender, EventArgs e)
{
RaiseBubbleEvent(this, new CommandEventArgs("Remove", null));
}

To handle this event, we need to use the ItemCommand event the repeater. The ItemCommand event is fired whenever a command is received by the repeater.



if (e.CommandName != "Remove") return;
var index = e.Item.ItemIndex;
var currentData = CurrentPersonList;
currentData.RemoveAt(index);
rptPeople.DataSource = currentData;
rptPeople.DataBind();

We check to see if the CommandName is the correct one, and if not we will just break out of the event. We find the ItemIndex of the item that raised the event and because we are using a list, it will be in the same position in that list. We can then remove the item at that position.

In the next post we will look at refactoring the code and making it generic so that you can use the same methods for all the repeaters etc.

No comments: